From 1cf7324c77540ad3b1acab9c6df61b58c69636e5 Mon Sep 17 00:00:00 2001 From: ale Date: Fri, 15 Aug 2025 02:27:27 +0200 Subject: [PATCH] initial commit Signed-off-by: ale --- .gitignore | 16 + README.md | 263 +++++++ app/.gitignore | 1 + app/build.gradle.kts | 67 ++ app/proguard-rules.pro | 21 + .../topcommand/ExampleInstrumentedTest.kt | 24 + app/src/main/AndroidManifest.xml | 27 + app/src/main/ic_launcher-playstore.png | Bin 0 -> 15823 bytes .../manalejandro/topcommand/MainActivity.kt | 20 + .../topcommand/model/ProcessInfo.kt | 29 + .../service/ProcessMonitorService.kt | 361 ++++++++++ .../topcommand/service/RootService.kt | 661 ++++++++++++++++++ .../ui/components/ProcessComponents.kt | 281 ++++++++ .../ui/components/ProcessDetailDialog.kt | 516 ++++++++++++++ .../ui/components/RootComponents.kt | 311 ++++++++ .../ui/screen/ProcessMonitorScreen.kt | 366 ++++++++++ .../manalejandro/topcommand/ui/theme/Color.kt | 28 + .../manalejandro/topcommand/ui/theme/Theme.kt | 84 +++ .../manalejandro/topcommand/ui/theme/Type.kt | 34 + .../topcommand/viewmodel/ProcessViewModel.kt | 269 +++++++ .../res/drawable/ic_launcher_background.xml | 170 +++++ .../res/drawable/ic_launcher_foreground.xml | 30 + .../res/mipmap-anydpi-v26/ic_launcher.xml | 5 + .../mipmap-anydpi-v26/ic_launcher_round.xml | 5 + app/src/main/res/mipmap-hdpi/ic_launcher.webp | Bin 0 -> 984 bytes .../mipmap-hdpi/ic_launcher_foreground.webp | Bin 0 -> 604 bytes .../res/mipmap-hdpi/ic_launcher_round.webp | Bin 0 -> 2786 bytes app/src/main/res/mipmap-mdpi/ic_launcher.webp | Bin 0 -> 676 bytes .../mipmap-mdpi/ic_launcher_foreground.webp | Bin 0 -> 388 bytes .../res/mipmap-mdpi/ic_launcher_round.webp | Bin 0 -> 1640 bytes .../main/res/mipmap-xhdpi/ic_launcher.webp | Bin 0 -> 1586 bytes .../mipmap-xhdpi/ic_launcher_foreground.webp | Bin 0 -> 870 bytes .../res/mipmap-xhdpi/ic_launcher_round.webp | Bin 0 -> 3950 bytes .../main/res/mipmap-xxhdpi/ic_launcher.webp | Bin 0 -> 2392 bytes .../mipmap-xxhdpi/ic_launcher_foreground.webp | Bin 0 -> 1316 bytes .../res/mipmap-xxhdpi/ic_launcher_round.webp | Bin 0 -> 6024 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.webp | Bin 0 -> 3082 bytes .../ic_launcher_foreground.webp | Bin 0 -> 1774 bytes .../res/mipmap-xxxhdpi/ic_launcher_round.webp | Bin 0 -> 8368 bytes app/src/main/res/values/colors.xml | 10 + .../res/values/ic_launcher_background.xml | 4 + app/src/main/res/values/strings.xml | 3 + app/src/main/res/values/themes.xml | 5 + app/src/main/res/xml/backup_rules.xml | 13 + .../main/res/xml/data_extraction_rules.xml | 19 + .../topcommand/ExampleUnitTest.kt | 17 + build.gradle.kts | 6 + gradle.properties | 23 + gradle/libs.versions.toml | 31 + gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 59203 bytes gradle/wrapper/gradle-wrapper.properties | 6 + gradlew | 185 +++++ gradlew.bat | 89 +++ screenshot.png | Bin 0 -> 102134 bytes settings.gradle.kts | 23 + 55 files changed, 4023 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 app/.gitignore create mode 100644 app/build.gradle.kts create mode 100644 app/proguard-rules.pro create mode 100644 app/src/androidTest/java/com/manalejandro/topcommand/ExampleInstrumentedTest.kt create mode 100644 app/src/main/AndroidManifest.xml create mode 100644 app/src/main/ic_launcher-playstore.png create mode 100644 app/src/main/java/com/manalejandro/topcommand/MainActivity.kt create mode 100644 app/src/main/java/com/manalejandro/topcommand/model/ProcessInfo.kt create mode 100644 app/src/main/java/com/manalejandro/topcommand/service/ProcessMonitorService.kt create mode 100644 app/src/main/java/com/manalejandro/topcommand/service/RootService.kt create mode 100644 app/src/main/java/com/manalejandro/topcommand/ui/components/ProcessComponents.kt create mode 100644 app/src/main/java/com/manalejandro/topcommand/ui/components/ProcessDetailDialog.kt create mode 100644 app/src/main/java/com/manalejandro/topcommand/ui/components/RootComponents.kt create mode 100644 app/src/main/java/com/manalejandro/topcommand/ui/screen/ProcessMonitorScreen.kt create mode 100644 app/src/main/java/com/manalejandro/topcommand/ui/theme/Color.kt create mode 100644 app/src/main/java/com/manalejandro/topcommand/ui/theme/Theme.kt create mode 100644 app/src/main/java/com/manalejandro/topcommand/ui/theme/Type.kt create mode 100644 app/src/main/java/com/manalejandro/topcommand/viewmodel/ProcessViewModel.kt create mode 100644 app/src/main/res/drawable/ic_launcher_background.xml create mode 100644 app/src/main/res/drawable/ic_launcher_foreground.xml create mode 100644 app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml create mode 100644 app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml create mode 100644 app/src/main/res/mipmap-hdpi/ic_launcher.webp create mode 100644 app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp create mode 100644 app/src/main/res/mipmap-hdpi/ic_launcher_round.webp create mode 100644 app/src/main/res/mipmap-mdpi/ic_launcher.webp create mode 100644 app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp create mode 100644 app/src/main/res/mipmap-mdpi/ic_launcher_round.webp create mode 100644 app/src/main/res/mipmap-xhdpi/ic_launcher.webp create mode 100644 app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp create mode 100644 app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp create mode 100644 app/src/main/res/mipmap-xxhdpi/ic_launcher.webp create mode 100644 app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp create mode 100644 app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp create mode 100644 app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp create mode 100644 app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp create mode 100644 app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp create mode 100644 app/src/main/res/values/colors.xml create mode 100644 app/src/main/res/values/ic_launcher_background.xml create mode 100644 app/src/main/res/values/strings.xml create mode 100644 app/src/main/res/values/themes.xml create mode 100644 app/src/main/res/xml/backup_rules.xml create mode 100644 app/src/main/res/xml/data_extraction_rules.xml create mode 100644 app/src/test/java/com/manalejandro/topcommand/ExampleUnitTest.kt create mode 100644 build.gradle.kts create mode 100644 gradle.properties create mode 100644 gradle/libs.versions.toml create mode 100644 gradle/wrapper/gradle-wrapper.jar create mode 100644 gradle/wrapper/gradle-wrapper.properties create mode 100755 gradlew create mode 100644 gradlew.bat create mode 100644 screenshot.png create mode 100644 settings.gradle.kts diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5c915a5 --- /dev/null +++ b/.gitignore @@ -0,0 +1,16 @@ +*.iml +.gradle +/local.properties +/.idea/caches +/.idea/libraries +/.idea/modules.xml +/.idea/workspace.xml +/.idea/navEditor.xml +/.idea/assetWizardSettings.xml +.DS_Store +/build +/captures +.externalNativeBuild +.cxx +local.properties +.idea/ diff --git a/README.md b/README.md new file mode 100644 index 0000000..cd82ed3 --- /dev/null +++ b/README.md @@ -0,0 +1,263 @@ +# Top Command - Android Process Monitor + +[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) +[![Android](https://img.shields.io/badge/Platform-Android-green.svg)](https://developer.android.com) +[![API](https://img.shields.io/badge/API-24%2B-brightgreen.svg?style=flat)](https://android-arsenal.com/api?level=24) + +Una aplicación moderna de monitoreo de procesos para Android inspirada en el comando `top` de Linux. Proporciona información detallada sobre todos los procesos del sistema con una interfaz intuitiva desarrollada en Jetpack Compose. + +![![Top Command Screenshot](screenshot.png)]( + +## 📱 Características + +### 🔍 **Monitoreo en Tiempo Real** +- Lista completa de procesos del sistema +- Actualización automática configurable (1-10 segundos) +- Información de CPU, memoria, estado y usuario para cada proceso +- Contadores de procesos y threads + +### 🔐 **Soporte para Root** +- **Sin Root**: Ve procesos del usuario actual +- **Con Root**: Acceso completo a todos los procesos del sistema +- Detección automática de disponibilidad de root +- Interfaz segura para solicitar permisos de superusuario + +### 📊 **Información Detallada del Sistema** +Al hacer clic en cualquier proceso se muestra: +- **Información básica**: PID, nombre, usuario, estado, prioridad +- **Rendimiento**: Uso de CPU y memoria con códigos de color +- **Detalles avanzados**: PID padre, tiempo de inicio, tiempo de CPU +- **Memoria**: Virtual, residente, compartida +- **Archivos**: Línea de comandos, archivos abiertos, directorio de trabajo +- **Red**: Conexiones TCP activas +- **Sistema**: Mapas de memoria, variables de entorno, límites + +### 🎨 **Diseño Atractivo** +- **Material 3 Design** con tema inspirado en terminal +- **Tema oscuro/claro** adaptativo +- **Colores contextuales** para estados y métricas de rendimiento +- **Animaciones fluidas** y transiciones suaves +- **Cards organizadas** para fácil lectura + +### 🔧 **Funcionalidades Avanzadas** +- **Búsqueda en tiempo real** por nombre, PID o usuario +- **Ordenación múltiple** por PID, nombre, CPU, memoria o usuario +- **Filtrado inteligente** con highlighting de resultados +- **Configuración personalizable** de intervalos de actualización +- **Información del sistema** (uptime, load average, memoria total) + +## 🚀 Compilación y Instalación + +### Prerrequisitos + +- **Android Studio** Hedgehog (2023.1.1) o superior +- **JDK 11** o superior +- **Android SDK** con API level 24+ (Android 7.0) +- **Gradle 8.13** o superior (incluido con el proyecto) + +### Clonar el Repositorio + +```bash +git clone https://github.com/tuusuario/top-command-android.git +cd top-command-android +``` + +### Compilar con Gradle + +#### Desde la línea de comandos: + +```bash +# Compilar versión debug +./gradlew assembleDebug + +# Compilar versión release +./gradlew assembleRelease + +# Limpiar y compilar +./gradlew clean assembleDebug + +# Instalar directamente en dispositivo conectado +./gradlew installDebug +``` + +#### En Windows: +```cmd +gradlew.bat assembleDebug +``` + +### Compilar desde Android Studio + +1. Abre **Android Studio** +2. Selecciona **"Open an Existing Project"** +3. Navega y selecciona la carpeta del proyecto +4. Espera a que Gradle sincronice las dependencias +5. Ejecuta con **Run 'app'** o **Ctrl+R** + +### Generar APK + +```bash +# APK debug (para desarrollo) +./gradlew assembleDebug +# Output: app/build/outputs/apk/debug/app-debug.apk + +# APK release (para distribución) +./gradlew assembleRelease +# Output: app/build/outputs/apk/release/app-release.apk +``` + +### Generar Bundle de Android (AAB) + +```bash +# Bundle para Google Play Store +./gradlew bundleRelease +# Output: app/build/outputs/bundle/release/app-release.aab +``` + +## 📋 Requisitos del Sistema + +### Para Compilación +- **SO**: Windows 10+, macOS 10.14+, o Linux +- **RAM**: Mínimo 8GB recomendado +- **Espacio**: 4GB libres para Android Studio + SDK + +### Para la Aplicación +- **Android**: 7.0 (API 24) o superior +- **Arquitectura**: ARM64, ARM32, x86, x86_64 +- **Permisos**: No requiere permisos especiales +- **Root** (opcional): Para acceso completo a procesos del sistema + +## 🛠️ Estructura del Proyecto + +``` +app/src/main/java/com/manalejandro/topcommand/ +├── model/ +│ ├── ProcessInfo.kt # Modelo de datos del proceso +│ └── SortBy.kt # Enumeración para ordenación +├── service/ +│ ├── ProcessMonitorService.kt # Servicio sin root +│ └── RootService.kt # Servicio con acceso root +├── ui/ +│ ├── components/ +│ │ ├── ProcessComponents.kt # Componentes de UI +│ │ ├── ProcessDetailDialog.kt # Diálogo de detalles +│ │ └── RootComponents.kt # Componentes de root +│ ├── screen/ +│ │ └── ProcessMonitorScreen.kt # Pantalla principal +│ └── theme/ +│ ├── Color.kt # Paleta de colores +│ └── Theme.kt # Tema Material +├── viewmodel/ +│ └── ProcessViewModel.kt # Lógica de negocio +└── MainActivity.kt # Actividad principal +``` + +## 🎯 Uso de la Aplicación + +### Navegación Básica +1. **Vista principal**: Lista de todos los procesos visibles +2. **Búsqueda**: Toca la barra de búsqueda para filtrar +3. **Ordenación**: Toca cualquier columna para ordenar +4. **Detalles**: Toca cualquier proceso para ver información completa +5. **Configuración**: Botón de ajustes en la barra superior + +### Acceso Root +1. La app detecta automáticamente si el dispositivo tiene root +2. Toca **"Enable Root Access"** en la tarjeta superior +3. Concede permisos cuando tu gestor de root lo solicite +4. Disfruta del acceso completo a todos los procesos del sistema + +### Funciones Avanzadas +- **Auto-refresh**: Configurable desde 1-10 segundos +- **Filtros**: Busca por nombre, PID, o usuario +- **Colores**: Verde (bajo), amarillo (medio), naranja/rojo (alto) +- **Información del sistema**: Visible solo con acceso root + +## 🧪 Testing + +```bash +# Ejecutar tests unitarios +./gradlew test + +# Tests de instrumentación (requiere dispositivo/emulador) +./gradlew connectedAndroidTest + +# Tests de UI +./gradlew connectedDebugAndroidTest +``` + +## 🏗️ Tecnologías Utilizadas + +- **[Jetpack Compose](https://developer.android.com/jetpack/compose)** - UI moderna y declarativa +- **[Material 3](https://m3.material.io/)** - Sistema de diseño +- **[ViewModel](https://developer.android.com/topic/libraries/architecture/viewmodel)** - Gestión de estado +- **[Kotlin Coroutines](https://kotlinlang.org/docs/coroutines-overview.html)** - Programación asíncrona +- **[StateFlow](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/-state-flow/)** - Flujo de datos reactivo + +## 🤝 Contribución + +¡Las contribuciones son bienvenidas! Por favor: + +1. Fork el proyecto +2. Crea una rama para tu feature (`git checkout -b feature/nueva-funcionalidad`) +3. Commit tus cambios (`git commit -am 'Agregar nueva funcionalidad'`) +4. Push a la rama (`git push origin feature/nueva-funcionalidad`) +5. Abre un Pull Request + +### Guías de Contribución +- Sigue las convenciones de código de Kotlin +- Documenta funciones públicas +- Incluye tests para nuevas funcionalidades +- Actualiza el README si es necesario + +## 📄 Licencia + +Este proyecto está licenciado bajo la Licencia MIT - ver el archivo [LICENSE](LICENSE) para detalles. + +``` +MIT License + +Copyright (c) 2024 Top Command Android + +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: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +``` + +## 👨‍💻 Autor + +**Alejandro** - [GitHub](https://github.com/tuusuario) + +## 📞 Soporte + +Si tienes problemas o preguntas: +- 🐛 **Reportar bugs**: [Issues](https://github.com/tuusuario/top-command-android/issues) +- 💡 **Sugerir funcionalidades**: [Discussions](https://github.com/tuusuario/top-command-android/discussions) +- 📧 **Contacto directo**: tu.email@ejemplo.com + +## 📈 Roadmap + +- [ ] Gráficos en tiempo real de CPU y memoria +- [ ] Exportar información de procesos +- [ ] Widgets de escritorio +- [ ] Notificaciones para procesos con alto uso +- [ ] Modo oscuro forzado +- [ ] Soporte para Android 15+ +- [ ] Filtros avanzados y guardado de configuración + +--- + +⭐ **¡No olvides dar una estrella al proyecto si te resultó útil!** ⭐ diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/app/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/app/build.gradle.kts b/app/build.gradle.kts new file mode 100644 index 0000000..55936ff --- /dev/null +++ b/app/build.gradle.kts @@ -0,0 +1,67 @@ +plugins { + alias(libs.plugins.android.application) + alias(libs.plugins.kotlin.android) + alias(libs.plugins.kotlin.compose) +} + +android { + namespace = "com.manalejandro.topcommand" + compileSdk = 34 + + defaultConfig { + applicationId = "com.manalejandro.topcommand" + minSdk = 24 + targetSdk = 34 + versionCode = 1 + versionName = "1.0" + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + } + + buildTypes { + release { + isMinifyEnabled = false + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) + } + } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + } + kotlinOptions { + jvmTarget = "11" + } + buildFeatures { + compose = true + } +} + +dependencies { + + implementation(libs.androidx.core.ktx) + implementation(libs.androidx.lifecycle.runtime.ktx) + implementation(libs.androidx.activity.compose) + implementation(platform(libs.androidx.compose.bom)) + implementation(libs.androidx.ui) + implementation(libs.androidx.ui.graphics) + implementation(libs.androidx.ui.tooling.preview) + implementation(libs.androidx.material3) + + // ViewModel and Lifecycle + implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.7.0") + implementation("androidx.compose.runtime:runtime-livedata:1.5.4") + + // Icons extended + implementation("androidx.compose.material:material-icons-extended:1.5.4") + + testImplementation(libs.junit) + androidTestImplementation(libs.androidx.junit) + androidTestImplementation(libs.androidx.espresso.core) + androidTestImplementation(platform(libs.androidx.compose.bom)) + androidTestImplementation(libs.androidx.ui.test.junit4) + debugImplementation(libs.androidx.ui.tooling) + debugImplementation(libs.androidx.ui.test.manifest) +} \ No newline at end of file diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100644 index 0000000..481bb43 --- /dev/null +++ b/app/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/app/src/androidTest/java/com/manalejandro/topcommand/ExampleInstrumentedTest.kt b/app/src/androidTest/java/com/manalejandro/topcommand/ExampleInstrumentedTest.kt new file mode 100644 index 0000000..3614f42 --- /dev/null +++ b/app/src/androidTest/java/com/manalejandro/topcommand/ExampleInstrumentedTest.kt @@ -0,0 +1,24 @@ +package com.manalejandro.topcommand + +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.manalejandro.topcommand", appContext.packageName) + } +} \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..a7501f8 --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/ic_launcher-playstore.png b/app/src/main/ic_launcher-playstore.png new file mode 100644 index 0000000000000000000000000000000000000000..a13f121e8f98336f3f730afb5a53c28e1bf54677 GIT binary patch literal 15823 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 Gd;bGLd6L-x literal 0 HcmV?d00001 diff --git a/app/src/main/java/com/manalejandro/topcommand/MainActivity.kt b/app/src/main/java/com/manalejandro/topcommand/MainActivity.kt new file mode 100644 index 0000000..134ff2a --- /dev/null +++ b/app/src/main/java/com/manalejandro/topcommand/MainActivity.kt @@ -0,0 +1,20 @@ +package com.manalejandro.topcommand + +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.activity.enableEdgeToEdge +import com.manalejandro.topcommand.ui.screen.ProcessMonitorScreen +import com.manalejandro.topcommand.ui.theme.TopCommandTheme + +class MainActivity : ComponentActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + enableEdgeToEdge() + setContent { + TopCommandTheme { + ProcessMonitorScreen() + } + } + } +} diff --git a/app/src/main/java/com/manalejandro/topcommand/model/ProcessInfo.kt b/app/src/main/java/com/manalejandro/topcommand/model/ProcessInfo.kt new file mode 100644 index 0000000..1e4238d --- /dev/null +++ b/app/src/main/java/com/manalejandro/topcommand/model/ProcessInfo.kt @@ -0,0 +1,29 @@ +package com.manalejandro.topcommand.model + +data class ProcessInfo( + val pid: Int, + val name: String, + val cpuUsage: Double, + val memoryUsage: Long, + val memoryPercentage: Double, + val user: String, + val state: String, + val priority: Int, + val threads: Int +) + +enum class SortBy { + PID, + NAME, + CPU, + MEMORY, + USER +} + +enum class ProcessState { + RUNNING, + SLEEPING, + STOPPED, + ZOMBIE, + UNKNOWN +} diff --git a/app/src/main/java/com/manalejandro/topcommand/service/ProcessMonitorService.kt b/app/src/main/java/com/manalejandro/topcommand/service/ProcessMonitorService.kt new file mode 100644 index 0000000..60810ef --- /dev/null +++ b/app/src/main/java/com/manalejandro/topcommand/service/ProcessMonitorService.kt @@ -0,0 +1,361 @@ +package com.manalejandro.topcommand.service + +import com.manalejandro.topcommand.model.ProcessInfo +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import java.io.File +import java.io.IOException + +class ProcessMonitorService { + + suspend fun getProcessList(): List = withContext(Dispatchers.IO) { + val processes = mutableListOf() + + try { + val procDir = File("/proc") + val pidDirs = procDir.listFiles { file -> + file.isDirectory && file.name.matches(Regex("\\d+")) + } ?: return@withContext emptyList() + + for (pidDir in pidDirs) { + try { + val pid = pidDir.name.toInt() + val processInfo = getProcessInfo(pid) + processInfo?.let { processes.add(it) } + } catch (e: Exception) { + // Skip processes that can't be read + continue + } + } + } catch (e: Exception) { + // Return empty list if can't access /proc + } + + processes.sortedByDescending { it.cpuUsage } + } + + private fun getProcessInfo(pid: Int): ProcessInfo? { + return try { + val statFile = File("/proc/$pid/stat") + val statusFile = File("/proc/$pid/status") + val cmdlineFile = File("/proc/$pid/cmdline") + + if (!statFile.exists() || !statusFile.exists()) return null + + val statContent = statFile.readText().split(" ") + val statusContent = statusFile.readText() + + val name = getProcessName(cmdlineFile, statContent) + val state = getProcessState(statContent[2]) + val priority = statContent.getOrNull(17)?.toIntOrNull() ?: 0 + val threads = statContent.getOrNull(19)?.toIntOrNull() ?: 1 + + val memoryInfo = getMemoryInfo(statusContent) + val user = getProcessUser(pid) + val cpuUsage = calculateCpuUsage(pid) + + ProcessInfo( + pid = pid, + name = name, + cpuUsage = cpuUsage, + memoryUsage = memoryInfo.first, + memoryPercentage = memoryInfo.second, + user = user, + state = state, + priority = priority, + threads = threads + ) + } catch (e: Exception) { + null + } + } + + private fun getProcessName(cmdlineFile: File, statContent: List): String { + return try { + if (cmdlineFile.exists()) { + val cmdline = cmdlineFile.readText().replace("\u0000", " ").trim() + if (cmdline.isNotEmpty()) { + cmdline.split(" ").first().split("/").last() + } else { + statContent[1].removeSurrounding("(", ")") + } + } else { + statContent[1].removeSurrounding("(", ")") + } + } catch (e: Exception) { + "unknown" + } + } + + private fun getProcessState(stateChar: String): String { + return when (stateChar) { + "R" -> "Running" + "S" -> "Sleeping" + "D" -> "Waiting" + "Z" -> "Zombie" + "T" -> "Stopped" + "t" -> "Tracing" + "W" -> "Paging" + "X", "x" -> "Dead" + "K" -> "Wakekill" + "P" -> "Parked" + else -> "Unknown" + } + } + + private fun getMemoryInfo(statusContent: String): Pair { + return try { + val vmRssLine = statusContent.lines().find { it.startsWith("VmRSS:") } + val vmSizeLine = statusContent.lines().find { it.startsWith("VmSize:") } + + val rss = vmRssLine?.split("\\s+".toRegex())?.get(1)?.toLongOrNull() ?: 0L + val size = vmSizeLine?.split("\\s+".toRegex())?.get(1)?.toLongOrNull() ?: 0L + + val rssBytes = rss * 1024 // Convert from KB to bytes + val totalMemory = getTotalMemory() + val percentage = if (totalMemory > 0) (rssBytes.toDouble() / totalMemory) * 100 else 0.0 + + Pair(rssBytes, percentage) + } catch (e: Exception) { + Pair(0L, 0.0) + } + } + + private fun getProcessUser(pid: Int): String { + return try { + val statusFile = File("/proc/$pid/status") + if (statusFile.exists()) { + val content = statusFile.readText() + val uidLine = content.lines().find { it.startsWith("Uid:") } + uidLine?.split("\\s+".toRegex())?.getOrNull(1) ?: "unknown" + } else { + "unknown" + } + } catch (e: Exception) { + "unknown" + } + } + + private fun calculateCpuUsage(pid: Int): Double { + return try { + // Simplified CPU usage calculation + // In a real implementation, you'd need to calculate this over time + val statFile = File("/proc/$pid/stat") + if (statFile.exists()) { + val statContent = statFile.readText().split(" ") + val utime = statContent.getOrNull(13)?.toLongOrNull() ?: 0L + val stime = statContent.getOrNull(14)?.toLongOrNull() ?: 0L + val totalTime = utime + stime + + // This is a simplified calculation + // Real CPU usage requires sampling over time + (totalTime % 100).toDouble() + } else { + 0.0 + } + } catch (e: Exception) { + 0.0 + } + } + + private fun getTotalMemory(): Long { + return try { + val meminfoFile = File("/proc/meminfo") + if (meminfoFile.exists()) { + val content = meminfoFile.readText() + val memTotalLine = content.lines().find { it.startsWith("MemTotal:") } + val memTotal = memTotalLine?.split("\\s+".toRegex())?.get(1)?.toLongOrNull() ?: 0L + memTotal * 1024 // Convert from KB to bytes + } else { + 0L + } + } catch (e: Exception) { + 0L + } + } + + suspend fun getProcessBasicDetails(pid: Int): ProcessDetailedInfo? = withContext(Dispatchers.IO) { + try { + val procDir = File("/proc/$pid") + if (!procDir.exists()) return@withContext null + + return@withContext ProcessDetailedInfo( + pid = pid, + parentPid = getBasicParentPid(pid), + commandLine = getBasicCommandLine(pid), + startTime = getBasicStartTime(pid), + cpuTime = getBasicCpuTime(pid), + virtualMemory = getBasicVirtualMemory(pid), + residentMemory = getBasicResidentMemory(pid), + sharedMemory = null, // Not accessible without root + terminal = getBasicTerminal(pid), + workingDirectory = getBasicWorkingDirectory(pid), + openFiles = getBasicOpenFiles(pid), + networkConnections = emptyList(), // Limited without root + memoryMaps = getBasicMemoryMaps(pid), + environment = emptyMap(), // Limited without root + limits = emptyMap() // Limited without root + ) + } catch (e: Exception) { + null + } + } + + private fun getBasicParentPid(pid: Int): Int? { + return try { + val statFile = File("/proc/$pid/stat") + if (!statFile.exists()) return null + val content = statFile.readText() + val parts = content.split(" ") + parts.getOrNull(3)?.toIntOrNull() + } catch (e: Exception) { + null + } + } + + private fun getBasicCommandLine(pid: Int): String { + return try { + val cmdlineFile = File("/proc/$pid/cmdline") + if (cmdlineFile.exists()) { + val cmdline = cmdlineFile.readText().replace("\u0000", " ").trim() + if (cmdline.isNotEmpty()) { + cmdline + } else { + val commFile = File("/proc/$pid/comm") + if (commFile.exists()) commFile.readText().trim() else "" + } + } else "" + } catch (e: Exception) { + "" + } + } + + private fun getBasicStartTime(pid: Int): String? { + return try { + val statFile = File("/proc/$pid/stat") + if (!statFile.exists()) return null + + val content = statFile.readText() + val parts = content.split(" ") + val starttime = parts.getOrNull(21)?.toLongOrNull() ?: return null + + val uptimeFile = File("/proc/uptime") + val uptime = uptimeFile.readText().split(" ")[0].toDouble() + + val clockTicks = 100 + val processAge = uptime - (starttime.toDouble() / clockTicks) + + "${String.format("%.2f", processAge)} seconds ago" + } catch (e: Exception) { + null + } + } + + private fun getBasicCpuTime(pid: Int): String? { + return try { + val statFile = File("/proc/$pid/stat") + if (!statFile.exists()) return null + + val content = statFile.readText() + val parts = content.split(" ") + val utime = parts.getOrNull(13)?.toLongOrNull() ?: 0L + val stime = parts.getOrNull(14)?.toLongOrNull() ?: 0L + val totalTime = utime + stime + + val clockTicks = 100 + val seconds = totalTime / clockTicks + val minutes = seconds / 60 + val hours = minutes / 60 + + when { + hours > 0 -> "${hours}:${String.format("%02d", minutes % 60)}:${String.format("%02d", seconds % 60)}" + minutes > 0 -> "${minutes}:${String.format("%02d", seconds % 60)}" + else -> "${seconds}s" + } + } catch (e: Exception) { + null + } + } + + private fun getBasicVirtualMemory(pid: Int): Long? { + return try { + val statusFile = File("/proc/$pid/status") + if (!statusFile.exists()) return null + + val content = statusFile.readText() + val vmSizeLine = content.lines().find { it.startsWith("VmSize:") } + val vmSize = vmSizeLine?.split("\\s+".toRegex())?.get(1)?.toLongOrNull() ?: return null + vmSize * 1024 + } catch (e: Exception) { + null + } + } + + private fun getBasicResidentMemory(pid: Int): Long? { + return try { + val statusFile = File("/proc/$pid/status") + if (!statusFile.exists()) return null + + val content = statusFile.readText() + val vmRssLine = content.lines().find { it.startsWith("VmRSS:") } + val vmRss = vmRssLine?.split("\\s+".toRegex())?.get(1)?.toLongOrNull() ?: return null + vmRss * 1024 + } catch (e: Exception) { + null + } + } + + private fun getBasicTerminal(pid: Int): String? { + return try { + val statFile = File("/proc/$pid/stat") + if (!statFile.exists()) return null + + val content = statFile.readText() + val parts = content.split(" ") + val tty = parts.getOrNull(6)?.toIntOrNull() ?: return null + + if (tty == 0) "?" else tty.toString() + } catch (e: Exception) { + null + } + } + + private fun getBasicWorkingDirectory(pid: Int): String? { + return try { + val cwdLink = File("/proc/$pid/cwd") + if (cwdLink.exists()) { + cwdLink.canonicalPath + } else null + } catch (e: Exception) { + null + } + } + + private fun getBasicOpenFiles(pid: Int): List { + return try { + val fdDir = File("/proc/$pid/fd") + if (!fdDir.exists()) return emptyList() + + fdDir.listFiles()?.mapNotNull { fdFile -> + try { + fdFile.canonicalPath + } catch (e: Exception) { + null + } + }?.distinct()?.take(15) ?: emptyList() + } catch (e: Exception) { + emptyList() + } + } + + private fun getBasicMemoryMaps(pid: Int): List { + return try { + val mapsFile = File("/proc/$pid/maps") + if (!mapsFile.exists()) return emptyList() + + mapsFile.readLines().take(8) + } catch (e: Exception) { + emptyList() + } + } +} diff --git a/app/src/main/java/com/manalejandro/topcommand/service/RootService.kt b/app/src/main/java/com/manalejandro/topcommand/service/RootService.kt new file mode 100644 index 0000000..401810c --- /dev/null +++ b/app/src/main/java/com/manalejandro/topcommand/service/RootService.kt @@ -0,0 +1,661 @@ +package com.manalejandro.topcommand.service + +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import java.io.BufferedReader +import java.io.DataOutputStream +import java.io.File +import java.io.InputStreamReader + +class RootService { + + private var isRootAvailable: Boolean? = null + private var suProcess: Process? = null + private var suOutput: DataOutputStream? = null + + suspend fun isRootAccessible(): Boolean = withContext(Dispatchers.IO) { + if (isRootAvailable != null) return@withContext isRootAvailable!! + + try { + val process = Runtime.getRuntime().exec("su") + val output = DataOutputStream(process.outputStream) + + // Test root access with a simple command + output.writeBytes("id\n") + output.flush() + output.writeBytes("exit\n") + output.flush() + + val exitCode = process.waitFor() + isRootAvailable = exitCode == 0 + + process.destroy() + return@withContext isRootAvailable!! + } catch (e: Exception) { + isRootAvailable = false + return@withContext false + } + } + + suspend fun requestRootAccess(): Boolean = withContext(Dispatchers.IO) { + try { + if (suProcess != null) { + // Root session already established + return@withContext true + } + + suProcess = Runtime.getRuntime().exec("su") + suOutput = DataOutputStream(suProcess!!.outputStream) + + // Test the connection + suOutput!!.writeBytes("echo 'root_access_granted'\n") + suOutput!!.flush() + + val reader = BufferedReader(InputStreamReader(suProcess!!.inputStream)) + val response = reader.readLine() + + return@withContext response?.contains("root_access_granted") == true + } catch (e: Exception) { + closeRootSession() + return@withContext false + } + } + + suspend fun executeRootCommand(command: String): String = withContext(Dispatchers.IO) { + try { + if (suProcess == null || suOutput == null) { + if (!requestRootAccess()) { + return@withContext "" + } + } + + suOutput!!.writeBytes("$command\n") + suOutput!!.flush() + + // Add a marker to know when the command output ends + val marker = "COMMAND_END_${System.currentTimeMillis()}" + suOutput!!.writeBytes("echo '$marker'\n") + suOutput!!.flush() + + val reader = BufferedReader(InputStreamReader(suProcess!!.inputStream)) + val output = StringBuilder() + var line: String? + + while (reader.readLine().also { line = it } != null) { + if (line == marker) break + output.append(line).append("\n") + } + + return@withContext output.toString() + } catch (e: Exception) { + closeRootSession() + return@withContext "" + } + } + + suspend fun getAllProcessesWithRoot(): List = withContext(Dispatchers.IO) { + val processes = mutableListOf() + + try { + // Get detailed process information using ps command with root + val psOutput = executeRootCommand("ps -A -o pid,ppid,user,comm,pcpu,pmem,vsz,rss,stat,tty,time,cmd") + + if (psOutput.isBlank()) { + // Fallback to basic ps command + val fallbackOutput = executeRootCommand("ps -A") + return@withContext parseBasicPsOutput(fallbackOutput) + } + + val lines = psOutput.trim().split("\n") + // Skip header line + for (i in 1 until lines.size) { + val line = lines[i].trim() + if (line.isEmpty()) continue + + try { + val processInfo = parseDetailedPsLine(line) + processInfo?.let { processes.add(it) } + } catch (e: Exception) { + // Skip malformed lines + continue + } + } + } catch (e: Exception) { + // Return empty list on error + } + + return@withContext processes.sortedByDescending { it.cpuUsage } + } + + private fun parseDetailedPsLine(line: String): ProcessRootInfo? { + try { + val parts = line.split(Regex("\\s+"), limit = 12) + if (parts.size < 11) return null + + val pid = parts[0].toIntOrNull() ?: return null + val ppid = parts[1].toIntOrNull() ?: 0 + val user = parts[2] + val comm = parts[3] + val pcpu = parts[4].toDoubleOrNull() ?: 0.0 + val pmem = parts[5].toDoubleOrNull() ?: 0.0 + val vsz = parts[6].toLongOrNull() ?: 0L + val rss = parts[7].toLongOrNull() ?: 0L + val stat = parts[8] + val tty = parts[9] + val time = parts[10] + val cmd = if (parts.size > 11) parts[11] else comm + + return ProcessRootInfo( + pid = pid, + parentPid = ppid, + user = user, + name = comm, + commandLine = cmd, + cpuUsage = pcpu, + memoryUsage = rss * 1024, // Convert KB to bytes + memoryPercentage = pmem, + virtualSize = vsz * 1024, // Convert KB to bytes + state = parseProcessState(stat), + terminal = tty, + cpuTime = time + ) + } catch (e: Exception) { + return null + } + } + + private fun parseBasicPsOutput(output: String): List { + val processes = mutableListOf() + val lines = output.trim().split("\n") + + // Skip header line + for (i in 1 until lines.size) { + val line = lines[i].trim() + if (line.isEmpty()) continue + + try { + val parts = line.split(Regex("\\s+")) + if (parts.size >= 2) { + val pid = parts[1].toIntOrNull() ?: continue + val name = parts.getOrNull(8) ?: "unknown" + + processes.add( + ProcessRootInfo( + pid = pid, + parentPid = 0, + user = parts.getOrNull(0) ?: "unknown", + name = name, + commandLine = name, + cpuUsage = 0.0, + memoryUsage = 0L, + memoryPercentage = 0.0, + virtualSize = 0L, + state = "Unknown", + terminal = parts.getOrNull(6) ?: "?", + cpuTime = parts.getOrNull(7) ?: "00:00:00" + ) + ) + } + } catch (e: Exception) { + continue + } + } + + return processes + } + + private fun parseProcessState(stat: String): String { + if (stat.isEmpty()) return "Unknown" + + return when (stat[0]) { + 'R' -> "Running" + 'S' -> "Sleeping" + 'D' -> "Waiting" + 'Z' -> "Zombie" + 'T' -> "Stopped" + 't' -> "Tracing" + 'W' -> "Paging" + 'X', 'x' -> "Dead" + 'K' -> "Wakekill" + 'P' -> "Parked" + else -> "Unknown ($stat)" + } + } + + suspend fun getSystemInfo(): SystemInfo = withContext(Dispatchers.IO) { + try { + val uptimeOutput = executeRootCommand("cat /proc/uptime") + val meminfoOutput = executeRootCommand("cat /proc/meminfo") + val cpuinfoOutput = executeRootCommand("cat /proc/cpuinfo") + val loadavgOutput = executeRootCommand("cat /proc/loadavg") + + return@withContext SystemInfo( + uptime = parseUptime(uptimeOutput), + loadAverage = parseLoadAverage(loadavgOutput), + memoryInfo = parseMemoryInfo(meminfoOutput), + cpuInfo = parseCpuInfo(cpuinfoOutput), + totalProcesses = getAllProcessesWithRoot().size + ) + } catch (e: Exception) { + return@withContext SystemInfo() + } + } + + private fun parseUptime(output: String): Long { + return try { + val parts = output.trim().split(" ") + (parts[0].toDouble() * 1000).toLong() + } catch (e: Exception) { + 0L + } + } + + private fun parseLoadAverage(output: String): Triple { + return try { + val parts = output.trim().split(" ") + Triple( + parts[0].toDouble(), + parts[1].toDouble(), + parts[2].toDouble() + ) + } catch (e: Exception) { + Triple(0.0, 0.0, 0.0) + } + } + + private fun parseMemoryInfo(output: String): MemoryInfo { + return try { + val lines = output.lines() + var total = 0L + var available = 0L + var free = 0L + var buffers = 0L + var cached = 0L + + lines.forEach { line -> + when { + line.startsWith("MemTotal:") -> total = extractMemoryValue(line) + line.startsWith("MemAvailable:") -> available = extractMemoryValue(line) + line.startsWith("MemFree:") -> free = extractMemoryValue(line) + line.startsWith("Buffers:") -> buffers = extractMemoryValue(line) + line.startsWith("Cached:") -> cached = extractMemoryValue(line) + } + } + + MemoryInfo( + total = total * 1024, + available = available * 1024, + free = free * 1024, + buffers = buffers * 1024, + cached = cached * 1024, + used = (total - available) * 1024 + ) + } catch (e: Exception) { + MemoryInfo() + } + } + + private fun extractMemoryValue(line: String): Long { + return try { + line.split(Regex("\\s+"))[1].toLong() + } catch (e: Exception) { + 0L + } + } + + private fun parseCpuInfo(output: String): String { + return try { + val lines = output.lines() + val modelLine = lines.find { it.startsWith("model name") } + modelLine?.split(":")?.get(1)?.trim() ?: "Unknown CPU" + } catch (e: Exception) { + "Unknown CPU" + } + } + + suspend fun getProcessDetailedInfo(pid: Int): ProcessDetailedInfo? = withContext(Dispatchers.IO) { + try { + val procDir = "/proc/$pid" + + // Check if process still exists + val statFile = File("$procDir/stat") + if (!statFile.exists()) return@withContext null + + val detailedInfo = ProcessDetailedInfo( + pid = pid, + parentPid = getParentPid(pid), + commandLine = getCommandLine(pid), + startTime = getStartTime(pid), + cpuTime = getCpuTime(pid), + virtualMemory = getVirtualMemory(pid), + residentMemory = getResidentMemory(pid), + sharedMemory = getSharedMemory(pid), + terminal = getTerminal(pid), + workingDirectory = getWorkingDirectory(pid), + openFiles = getOpenFiles(pid), + networkConnections = getNetworkConnections(pid), + memoryMaps = getMemoryMaps(pid), + environment = getEnvironment(pid), + limits = getLimits(pid) + ) + + return@withContext detailedInfo + } catch (e: Exception) { + return@withContext null + } + } + + private suspend fun getParentPid(pid: Int): Int? { + return try { + val statContent = executeRootCommand("cat /proc/$pid/stat") + if (statContent.isBlank()) return null + val parts = statContent.split(" ") + parts.getOrNull(3)?.toIntOrNull() + } catch (e: Exception) { + null + } + } + + private suspend fun getCommandLine(pid: Int): String { + return try { + val cmdline = executeRootCommand("cat /proc/$pid/cmdline 2>/dev/null") + if (cmdline.isBlank()) { + // Fallback to comm if cmdline is empty + executeRootCommand("cat /proc/$pid/comm 2>/dev/null").trim() + } else { + cmdline.replace("\u0000", " ").trim() + } + } catch (e: Exception) { + "" + } + } + + private suspend fun getStartTime(pid: Int): String? { + return try { + val statContent = executeRootCommand("cat /proc/$pid/stat 2>/dev/null") + if (statContent.isBlank()) return null + + val parts = statContent.split(" ") + val starttime = parts.getOrNull(21)?.toLongOrNull() ?: return null + + // Convert to readable time (simplified) + val uptimeContent = executeRootCommand("cat /proc/uptime 2>/dev/null") + val uptime = uptimeContent.split(" ").getOrNull(0)?.toDoubleOrNull() ?: return null + + val clockTicks = 100 // Assuming 100 Hz + val processAge = uptime - (starttime.toDouble() / clockTicks) + + "${String.format("%.2f", processAge)} seconds ago" + } catch (e: Exception) { + null + } + } + + private suspend fun getCpuTime(pid: Int): String? { + return try { + val statContent = executeRootCommand("cat /proc/$pid/stat 2>/dev/null") + if (statContent.isBlank()) return null + + val parts = statContent.split(" ") + val utime = parts.getOrNull(13)?.toLongOrNull() ?: 0L + val stime = parts.getOrNull(14)?.toLongOrNull() ?: 0L + val totalTime = utime + stime + + val clockTicks = 100 // Assuming 100 Hz + val seconds = totalTime / clockTicks + val minutes = seconds / 60 + val hours = minutes / 60 + + when { + hours > 0 -> "${hours}:${String.format("%02d", minutes % 60)}:${String.format("%02d", seconds % 60)}" + minutes > 0 -> "${minutes}:${String.format("%02d", seconds % 60)}" + else -> "${seconds}s" + } + } catch (e: Exception) { + null + } + } + + private suspend fun getVirtualMemory(pid: Int): Long? { + return try { + val statusContent = executeRootCommand("cat /proc/$pid/status 2>/dev/null") + if (statusContent.isBlank()) return null + + val vmSizeLine = statusContent.lines().find { it.startsWith("VmSize:") } + val vmSize = vmSizeLine?.split("\\s+".toRegex())?.get(1)?.toLongOrNull() ?: return null + vmSize * 1024 // Convert from KB to bytes + } catch (e: Exception) { + null + } + } + + private suspend fun getResidentMemory(pid: Int): Long? { + return try { + val statusContent = executeRootCommand("cat /proc/$pid/status 2>/dev/null") + if (statusContent.isBlank()) return null + + val vmRssLine = statusContent.lines().find { it.startsWith("VmRSS:") } + val vmRss = vmRssLine?.split("\\s+".toRegex())?.get(1)?.toLongOrNull() ?: return null + vmRss * 1024 // Convert from KB to bytes + } catch (e: Exception) { + null + } + } + + private suspend fun getSharedMemory(pid: Int): Long? { + return try { + val statusContent = executeRootCommand("cat /proc/$pid/status 2>/dev/null") + if (statusContent.isBlank()) return null + + val vmLibLine = statusContent.lines().find { it.startsWith("VmLib:") } + val vmLib = vmLibLine?.split("\\s+".toRegex())?.get(1)?.toLongOrNull() ?: return null + vmLib * 1024 // Convert from KB to bytes + } catch (e: Exception) { + null + } + } + + private suspend fun getTerminal(pid: Int): String? { + return try { + val statContent = executeRootCommand("cat /proc/$pid/stat 2>/dev/null") + if (statContent.isBlank()) return null + + val parts = statContent.split(" ") + val tty = parts.getOrNull(6)?.toIntOrNull() ?: return null + + if (tty == 0) "?" else tty.toString() + } catch (e: Exception) { + null + } + } + + private suspend fun getWorkingDirectory(pid: Int): String? { + return try { + val cwd = executeRootCommand("readlink /proc/$pid/cwd 2>/dev/null") + if (cwd.isBlank()) null else cwd.trim() + } catch (e: Exception) { + null + } + } + + private suspend fun getOpenFiles(pid: Int): List { + return try { + val lsofOutput = executeRootCommand("ls -la /proc/$pid/fd 2>/dev/null") + if (lsofOutput.isBlank()) return emptyList() + + lsofOutput.lines() + .drop(1) // Skip header + .mapNotNull { line -> + val parts = line.split("->") + if (parts.size >= 2) { + parts[1].trim() + } else null + } + .filter { it.isNotBlank() } + .distinct() + .take(20) // Limit to avoid too many files + } catch (e: Exception) { + emptyList() + } + } + + private suspend fun getNetworkConnections(pid: Int): List { + return try { + val connections = mutableListOf() + + // Check TCP connections + val tcpContent = executeRootCommand("cat /proc/net/tcp 2>/dev/null") + val tcp6Content = executeRootCommand("cat /proc/net/tcp6 2>/dev/null") + + // This is a simplified version - in real implementation you'd need to + // parse the network files and match inodes with the process fd + connections.addAll(parseNetworkConnections(tcpContent, "TCP")) + connections.addAll(parseNetworkConnections(tcp6Content, "TCP6")) + + connections.take(10) // Limit connections shown + } catch (e: Exception) { + emptyList() + } + } + + private fun parseNetworkConnections(content: String, protocol: String): List { + return try { + content.lines() + .drop(1) // Skip header + .take(5) // Limit for performance + .mapNotNull { line -> + val parts = line.trim().split("\\s+".toRegex()) + if (parts.size >= 4) { + val localAddr = parts.getOrNull(1) + val remoteAddr = parts.getOrNull(2) + val state = parts.getOrNull(3) + + if (localAddr != null && remoteAddr != null) { + "$protocol: $localAddr -> $remoteAddr [$state]" + } else null + } else null + } + } catch (e: Exception) { + emptyList() + } + } + + private suspend fun getMemoryMaps(pid: Int): List { + return try { + val mapsContent = executeRootCommand("cat /proc/$pid/maps 2>/dev/null") + if (mapsContent.isBlank()) return emptyList() + + mapsContent.lines() + .filter { it.isNotBlank() } + .take(10) // Limit to first 10 mappings + } catch (e: Exception) { + emptyList() + } + } + + private suspend fun getEnvironment(pid: Int): Map { + return try { + val environContent = executeRootCommand("cat /proc/$pid/environ 2>/dev/null") + if (environContent.isBlank()) return emptyMap() + + environContent.split("\u0000") + .mapNotNull { env -> + val parts = env.split("=", limit = 2) + if (parts.size == 2) { + parts[0] to parts[1] + } else null + } + .take(20) // Limit environment variables + .toMap() + } catch (e: Exception) { + emptyMap() + } + } + + private suspend fun getLimits(pid: Int): Map { + return try { + val limitsContent = executeRootCommand("cat /proc/$pid/limits 2>/dev/null") + if (limitsContent.isBlank()) return emptyMap() + + limitsContent.lines() + .drop(1) // Skip header + .mapNotNull { line -> + val parts = line.split("\\s+".toRegex()) + if (parts.size >= 3) { + val limit = parts[0] + val soft = parts[1] + val hard = parts[2] + limit to "$soft / $hard" + } else null + } + .take(10) + .toMap() + } catch (e: Exception) { + emptyMap() + } + } + + suspend fun closeRootSession() { + try { + suOutput?.writeBytes("exit\n") + suOutput?.flush() + suOutput?.close() + suProcess?.destroy() + } catch (e: Exception) { + // Ignore cleanup errors + } finally { + suOutput = null + suProcess = null + } + } +} + +data class ProcessRootInfo( + val pid: Int, + val parentPid: Int, + val user: String, + val name: String, + val commandLine: String, + val cpuUsage: Double, + val memoryUsage: Long, + val memoryPercentage: Double, + val virtualSize: Long, + val state: String, + val terminal: String, + val cpuTime: String +) + +data class SystemInfo( + val uptime: Long = 0L, + val loadAverage: Triple = Triple(0.0, 0.0, 0.0), + val memoryInfo: MemoryInfo = MemoryInfo(), + val cpuInfo: String = "Unknown", + val totalProcesses: Int = 0 +) + +data class MemoryInfo( + val total: Long = 0L, + val available: Long = 0L, + val free: Long = 0L, + val buffers: Long = 0L, + val cached: Long = 0L, + val used: Long = 0L +) + +data class ProcessDetailedInfo( + val pid: Int, + val parentPid: Int?, + val commandLine: String, + val startTime: String?, + val cpuTime: String?, + val virtualMemory: Long?, + val residentMemory: Long?, + val sharedMemory: Long?, + val terminal: String?, + val workingDirectory: String?, + val openFiles: List, + val networkConnections: List, + val memoryMaps: List, + val environment: Map, + val limits: Map +) diff --git a/app/src/main/java/com/manalejandro/topcommand/ui/components/ProcessComponents.kt b/app/src/main/java/com/manalejandro/topcommand/ui/components/ProcessComponents.kt new file mode 100644 index 0000000..e479b7d --- /dev/null +++ b/app/src/main/java/com/manalejandro/topcommand/ui/components/ProcessComponents.kt @@ -0,0 +1,281 @@ +package com.manalejandro.topcommand.ui.components + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.KeyboardArrowDown +import androidx.compose.material.icons.filled.KeyboardArrowUp +import androidx.compose.material3.* +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import com.manalejandro.topcommand.model.ProcessInfo +import com.manalejandro.topcommand.model.SortBy + +@Composable +fun ProcessItem( + process: ProcessInfo, + onClick: () -> Unit = {}, + modifier: Modifier = Modifier +) { + Card( + modifier = modifier + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 4.dp) + .clickable { onClick() }, + elevation = CardDefaults.cardElevation(defaultElevation = 2.dp), + colors = CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.surface + ) + ) { + Column( + modifier = Modifier.padding(16.dp) + ) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Column(modifier = Modifier.weight(1f)) { + Text( + text = process.name, + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.Bold, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + Text( + text = "PID: ${process.pid} • User: ${process.user}", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + + ProcessStateChip(state = process.state) + } + + Spacer(modifier = Modifier.height(12.dp)) + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(16.dp) + ) { + MetricCard( + label = "CPU", + value = "${String.format("%.1f", process.cpuUsage)}%", + color = getCpuColor(process.cpuUsage), + modifier = Modifier.weight(1f) + ) + + MetricCard( + label = "Memory", + value = formatMemory(process.memoryUsage), + color = getMemoryColor(process.memoryPercentage), + modifier = Modifier.weight(1f) + ) + + MetricCard( + label = "Threads", + value = process.threads.toString(), + color = MaterialTheme.colorScheme.primary, + modifier = Modifier.weight(1f) + ) + } + } + } +} + +@Composable +fun ProcessStateChip( + state: String, + modifier: Modifier = Modifier +) { + val (backgroundColor, contentColor) = when (state) { + "Running" -> Pair(Color(0xFF4CAF50), Color.White) + "Sleeping" -> Pair(Color(0xFF2196F3), Color.White) + "Zombie" -> Pair(Color(0xFFF44336), Color.White) + "Stopped" -> Pair(Color(0xFFFF9800), Color.White) + else -> Pair(MaterialTheme.colorScheme.surfaceVariant, MaterialTheme.colorScheme.onSurfaceVariant) + } + + Box( + modifier = modifier + .clip(RoundedCornerShape(12.dp)) + .background(backgroundColor) + .padding(horizontal = 8.dp, vertical = 4.dp) + ) { + Text( + text = state, + style = MaterialTheme.typography.labelSmall, + color = contentColor, + fontWeight = FontWeight.Medium + ) + } +} + +@Composable +fun MetricCard( + label: String, + value: String, + color: Color, + modifier: Modifier = Modifier +) { + Card( + modifier = modifier, + colors = CardDefaults.cardColors( + containerColor = color.copy(alpha = 0.1f) + ) + ) { + Column( + modifier = Modifier.padding(8.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text( + text = label, + style = MaterialTheme.typography.labelSmall, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + Text( + text = value, + style = MaterialTheme.typography.bodyMedium, + fontWeight = FontWeight.Bold, + color = color + ) + } + } +} + +@Composable +fun SortHeader( + sortBy: SortBy, + isAscending: Boolean, + onSortChange: (SortBy) -> Unit, + modifier: Modifier = Modifier +) { + Card( + modifier = modifier + .fillMaxWidth() + .padding(16.dp), + colors = CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.primaryContainer + ) + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(12.dp), + horizontalArrangement = Arrangement.SpaceBetween + ) { + SortButton( + text = "PID", + sortBy = SortBy.PID, + currentSort = sortBy, + isAscending = isAscending, + onClick = onSortChange, + modifier = Modifier.weight(1f) + ) + SortButton( + text = "Name", + sortBy = SortBy.NAME, + currentSort = sortBy, + isAscending = isAscending, + onClick = onSortChange, + modifier = Modifier.weight(1f) + ) + SortButton( + text = "CPU", + sortBy = SortBy.CPU, + currentSort = sortBy, + isAscending = isAscending, + onClick = onSortChange, + modifier = Modifier.weight(1f) + ) + SortButton( + text = "Memory", + sortBy = SortBy.MEMORY, + currentSort = sortBy, + isAscending = isAscending, + onClick = onSortChange, + modifier = Modifier.weight(1f) + ) + SortButton( + text = "User", + sortBy = SortBy.USER, + currentSort = sortBy, + isAscending = isAscending, + onClick = onSortChange, + modifier = Modifier.weight(1f) + ) + } + } +} + +@Composable +fun SortButton( + text: String, + sortBy: SortBy, + currentSort: SortBy, + isAscending: Boolean, + onClick: (SortBy) -> Unit, + modifier: Modifier = Modifier +) { + val isSelected = currentSort == sortBy + + Row( + modifier = modifier + .clickable { onClick(sortBy) } + .padding(4.dp), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = text, + style = MaterialTheme.typography.labelMedium, + fontWeight = if (isSelected) FontWeight.Bold else FontWeight.Normal, + color = if (isSelected) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurfaceVariant + ) + + if (isSelected) { + Icon( + imageVector = if (isAscending) Icons.Default.KeyboardArrowUp else Icons.Default.KeyboardArrowDown, + contentDescription = if (isAscending) "Ascending" else "Descending", + modifier = Modifier.size(16.dp), + tint = MaterialTheme.colorScheme.primary + ) + } + } +} + +private fun getCpuColor(cpuUsage: Double): Color { + return when { + cpuUsage > 80 -> Color(0xFFF44336) // Red + cpuUsage > 50 -> Color(0xFFFF9800) // Orange + cpuUsage > 20 -> Color(0xFFFFEB3B) // Yellow + else -> Color(0xFF4CAF50) // Green + } +} + +private fun getMemoryColor(memoryPercentage: Double): Color { + return when { + memoryPercentage > 80 -> Color(0xFFF44336) // Red + memoryPercentage > 50 -> Color(0xFFFF9800) // Orange + memoryPercentage > 20 -> Color(0xFFFFEB3B) // Yellow + else -> Color(0xFF4CAF50) // Green + } +} + +private fun formatMemory(bytes: Long): String { + return when { + bytes >= 1_073_741_824 -> "${String.format("%.1f", bytes / 1_073_741_824.0)} GB" + bytes >= 1_048_576 -> "${String.format("%.1f", bytes / 1_048_576.0)} MB" + bytes >= 1024 -> "${String.format("%.1f", bytes / 1024.0)} KB" + else -> "$bytes B" + } +} diff --git a/app/src/main/java/com/manalejandro/topcommand/ui/components/ProcessDetailDialog.kt b/app/src/main/java/com/manalejandro/topcommand/ui/components/ProcessDetailDialog.kt new file mode 100644 index 0000000..e0f90a4 --- /dev/null +++ b/app/src/main/java/com/manalejandro/topcommand/ui/components/ProcessDetailDialog.kt @@ -0,0 +1,516 @@ +package com.manalejandro.topcommand.ui.components + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Close +import androidx.compose.material.icons.filled.Computer +import androidx.compose.material.icons.filled.Info +import androidx.compose.material.icons.filled.Memory +import androidx.compose.material.icons.filled.Person +import androidx.compose.material.icons.filled.Speed +import androidx.compose.material.icons.filled.Timer +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.Dialog +import androidx.compose.ui.window.DialogProperties +import com.manalejandro.topcommand.model.ProcessInfo +import com.manalejandro.topcommand.service.ProcessDetailedInfo +import kotlinx.coroutines.launch + +@Composable +fun ProcessDetailDialog( + process: ProcessInfo, + detailedInfo: ProcessDetailedInfo?, + onDismiss: () -> Unit, + onRefreshDetails: (Int) -> Unit, + isLoadingDetails: Boolean = false +) { + val scope = rememberCoroutineScope() + + Dialog( + onDismissRequest = onDismiss, + properties = DialogProperties( + dismissOnBackPress = true, + dismissOnClickOutside = true, + usePlatformDefaultWidth = false + ) + ) { + Card( + modifier = Modifier + .fillMaxWidth() + .fillMaxHeight(0.9f) + .padding(16.dp), + colors = CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.surface + ) + ) { + Column( + modifier = Modifier.fillMaxSize() + ) { + // Header + ProcessDetailHeader( + process = process, + onClose = onDismiss, + onRefresh = { + scope.launch { + onRefreshDetails(process.pid) + } + }, + isRefreshing = isLoadingDetails + ) + + // Content + Column( + modifier = Modifier + .fillMaxSize() + .verticalScroll(rememberScrollState()) + .padding(16.dp), + verticalArrangement = Arrangement.spacedBy(12.dp) + ) { + // Basic Information + ProcessBasicInfoSection(process = process) + + // Performance Metrics + ProcessPerformanceSection(process = process) + + // Detailed Information (if available) + if (detailedInfo != null) { + ProcessAdvancedInfoSection(detailedInfo = detailedInfo) + + if (detailedInfo.commandLine.isNotBlank()) { + ProcessCommandSection(detailedInfo = detailedInfo) + } + + if (detailedInfo.openFiles.isNotEmpty()) { + ProcessFilesSection(detailedInfo = detailedInfo) + } + + if (detailedInfo.networkConnections.isNotEmpty()) { + ProcessNetworkSection(detailedInfo = detailedInfo) + } + + ProcessMemoryDetailsSection(detailedInfo = detailedInfo) + } else if (isLoadingDetails) { + Box( + modifier = Modifier + .fillMaxWidth() + .padding(24.dp), + contentAlignment = Alignment.Center + ) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + CircularProgressIndicator() + Text( + text = "Loading detailed information...", + style = MaterialTheme.typography.bodyMedium + ) + } + } + } else { + Card( + colors = CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.surfaceVariant + ) + ) { + Column( + modifier = Modifier.padding(16.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text( + text = "Detailed information not available", + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + Text( + text = "Enable root access for more details", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f) + ) + } + } + } + } + } + } + } +} + +@Composable +fun ProcessDetailHeader( + process: ProcessInfo, + onClose: () -> Unit, + onRefresh: () -> Unit, + isRefreshing: Boolean +) { + Card( + colors = CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.primaryContainer + ), + shape = RoundedCornerShape(topStart = 12.dp, topEnd = 12.dp, bottomStart = 0.dp, bottomEnd = 0.dp) + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Column(modifier = Modifier.weight(1f)) { + Text( + text = process.name, + style = MaterialTheme.typography.headlineSmall, + fontWeight = FontWeight.Bold, + color = MaterialTheme.colorScheme.onPrimaryContainer + ) + Text( + text = "PID: ${process.pid}", + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onPrimaryContainer.copy(alpha = 0.8f) + ) + } + + Row( + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + IconButton( + onClick = onRefresh, + enabled = !isRefreshing + ) { + if (isRefreshing) { + CircularProgressIndicator( + modifier = Modifier.size(24.dp), + strokeWidth = 2.dp, + color = MaterialTheme.colorScheme.onPrimaryContainer + ) + } else { + Icon( + imageVector = Icons.Default.Timer, + contentDescription = "Refresh", + tint = MaterialTheme.colorScheme.onPrimaryContainer + ) + } + } + + IconButton(onClick = onClose) { + Icon( + imageVector = Icons.Default.Close, + contentDescription = "Close", + tint = MaterialTheme.colorScheme.onPrimaryContainer + ) + } + } + } + } +} + +@Composable +fun ProcessBasicInfoSection(process: ProcessInfo) { + DetailSection( + title = "Basic Information", + icon = Icons.Default.Info + ) { + DetailRow("Process ID", process.pid.toString()) + DetailRow("Name", process.name) + DetailRow("User", process.user) + DetailRow("State", process.state) + DetailRow("Priority", process.priority.toString()) + DetailRow("Threads", process.threads.toString()) + } +} + +@Composable +fun ProcessPerformanceSection(process: ProcessInfo) { + DetailSection( + title = "Performance", + icon = Icons.Default.Speed + ) { + DetailRow( + "CPU Usage", + "${String.format("%.1f", process.cpuUsage)}%", + valueColor = getCpuColor(process.cpuUsage) + ) + DetailRow( + "Memory Usage", + formatMemory(process.memoryUsage), + valueColor = getMemoryColor(process.memoryPercentage) + ) + DetailRow( + "Memory %", + "${String.format("%.2f", process.memoryPercentage)}%", + valueColor = getMemoryColor(process.memoryPercentage) + ) + } +} + +@Composable +fun ProcessAdvancedInfoSection(detailedInfo: ProcessDetailedInfo) { + DetailSection( + title = "Advanced Information", + icon = Icons.Default.Computer + ) { + detailedInfo.parentPid?.let { + DetailRow("Parent PID", it.toString()) + } + + detailedInfo.startTime?.let { + DetailRow("Start Time", it) + } + + detailedInfo.cpuTime?.let { + DetailRow("CPU Time", it) + } + + detailedInfo.virtualMemory?.let { + DetailRow("Virtual Memory", formatMemory(it)) + } + + detailedInfo.residentMemory?.let { + DetailRow("Resident Memory", formatMemory(it)) + } + + detailedInfo.sharedMemory?.let { + DetailRow("Shared Memory", formatMemory(it)) + } + + detailedInfo.terminal?.let { + DetailRow("Terminal", it) + } + + detailedInfo.workingDirectory?.let { + DetailRow("Working Directory", it) + } + } +} + +@Composable +fun ProcessCommandSection(detailedInfo: ProcessDetailedInfo) { + DetailSection( + title = "Command Line", + icon = Icons.Default.Computer + ) { + Card( + colors = CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f) + ) + ) { + Text( + text = detailedInfo.commandLine, + modifier = Modifier.padding(12.dp), + style = MaterialTheme.typography.bodyMedium, + fontFamily = FontFamily.Monospace, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + } +} + +@Composable +fun ProcessFilesSection(detailedInfo: ProcessDetailedInfo) { + DetailSection( + title = "Open Files (${detailedInfo.openFiles.size})", + icon = Icons.Default.Info + ) { + detailedInfo.openFiles.take(10).forEach { file -> + Card( + colors = CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.3f) + ), + modifier = Modifier.fillMaxWidth() + ) { + Text( + text = file, + modifier = Modifier.padding(8.dp), + style = MaterialTheme.typography.bodySmall, + fontFamily = FontFamily.Monospace + ) + } + } + if (detailedInfo.openFiles.size > 10) { + Text( + text = "... and ${detailedInfo.openFiles.size - 10} more files", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f) + ) + } + } +} + +@Composable +fun ProcessNetworkSection(detailedInfo: ProcessDetailedInfo) { + DetailSection( + title = "Network Connections (${detailedInfo.networkConnections.size})", + icon = Icons.Default.Computer + ) { + detailedInfo.networkConnections.forEach { connection -> + Card( + colors = CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.3f) + ), + modifier = Modifier.fillMaxWidth() + ) { + Text( + text = connection, + modifier = Modifier.padding(8.dp), + style = MaterialTheme.typography.bodySmall, + fontFamily = FontFamily.Monospace + ) + } + } + } +} + +@Composable +fun ProcessMemoryDetailsSection(detailedInfo: ProcessDetailedInfo) { + DetailSection( + title = "Memory Details", + icon = Icons.Default.Memory + ) { + detailedInfo.virtualMemory?.let { + DetailRow("Virtual Memory", formatMemory(it)) + } + + detailedInfo.residentMemory?.let { + DetailRow("Resident Set Size", formatMemory(it)) + } + + detailedInfo.sharedMemory?.let { + DetailRow("Shared Memory", formatMemory(it)) + } + + detailedInfo.memoryMaps?.let { maps -> + Text( + text = "Memory Maps (${maps.size})", + style = MaterialTheme.typography.titleSmall, + fontWeight = FontWeight.Medium, + modifier = Modifier.padding(top = 8.dp) + ) + + maps.take(5).forEach { map -> + Card( + colors = CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.3f) + ), + modifier = Modifier.fillMaxWidth() + ) { + Text( + text = map, + modifier = Modifier.padding(8.dp), + style = MaterialTheme.typography.bodySmall, + fontFamily = FontFamily.Monospace + ) + } + } + if (maps.size > 5) { + Text( + text = "... and ${maps.size - 5} more mappings", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f) + ) + } + } + } +} + +@Composable +fun DetailSection( + title: String, + icon: ImageVector, + content: @Composable ColumnScope.() -> Unit +) { + Card( + colors = CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.3f) + ) + ) { + Column( + modifier = Modifier.padding(16.dp), + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + Icon( + imageVector = icon, + contentDescription = title, + modifier = Modifier.size(20.dp), + tint = MaterialTheme.colorScheme.primary + ) + Text( + text = title, + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.Bold + ) + } + + content() + } + } +} + +@Composable +fun DetailRow( + label: String, + value: String, + valueColor: Color = MaterialTheme.colorScheme.onSurface +) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = label, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.weight(1f) + ) + Text( + text = value, + style = MaterialTheme.typography.bodyMedium, + fontWeight = FontWeight.Medium, + color = valueColor + ) + } +} + +private fun getCpuColor(cpuUsage: Double): Color { + return when { + cpuUsage > 80 -> Color(0xFFF44336) // Red + cpuUsage > 50 -> Color(0xFFFF9800) // Orange + cpuUsage > 20 -> Color(0xFFFFEB3B) // Yellow + else -> Color(0xFF4CAF50) // Green + } +} + +private fun getMemoryColor(memoryPercentage: Double): Color { + return when { + memoryPercentage > 80 -> Color(0xFFF44336) // Red + memoryPercentage > 50 -> Color(0xFFFF9800) // Orange + memoryPercentage > 20 -> Color(0xFFFFEB3B) // Yellow + else -> Color(0xFF4CAF50) // Green + } +} + +private fun formatMemory(bytes: Long): String { + return when { + bytes >= 1_073_741_824 -> "${String.format("%.1f", bytes / 1_073_741_824.0)} GB" + bytes >= 1_048_576 -> "${String.format("%.1f", bytes / 1_048_576.0)} MB" + bytes >= 1024 -> "${String.format("%.1f", bytes / 1024.0)} KB" + else -> "$bytes B" + } +} diff --git a/app/src/main/java/com/manalejandro/topcommand/ui/components/RootComponents.kt b/app/src/main/java/com/manalejandro/topcommand/ui/components/RootComponents.kt new file mode 100644 index 0000000..bf5c530 --- /dev/null +++ b/app/src/main/java/com/manalejandro/topcommand/ui/components/RootComponents.kt @@ -0,0 +1,311 @@ +package com.manalejandro.topcommand.ui.components + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Security +import androidx.compose.material.icons.filled.Warning +import androidx.compose.material.icons.filled.Computer +import androidx.compose.material.icons.filled.Memory +import androidx.compose.material.icons.filled.Speed +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import com.manalejandro.topcommand.service.SystemInfo +import java.text.SimpleDateFormat +import java.util.* +import kotlin.time.Duration.Companion.milliseconds + +@Composable +fun RootAccessCard( + isRootAvailable: Boolean?, + isRootEnabled: Boolean, + onRequestRoot: () -> Unit, + onDisableRoot: () -> Unit, + rootError: String?, + modifier: Modifier = Modifier +) { + Card( + modifier = modifier + .fillMaxWidth() + .padding(16.dp), + colors = CardDefaults.cardColors( + containerColor = when { + rootError != null -> MaterialTheme.colorScheme.errorContainer + isRootEnabled -> Color(0xFF1B5E20) + isRootAvailable == true -> MaterialTheme.colorScheme.primaryContainer + else -> MaterialTheme.colorScheme.surfaceVariant + } + ) + ) { + Column( + modifier = Modifier.padding(16.dp), + verticalArrangement = Arrangement.spacedBy(12.dp) + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + Icon( + imageVector = if (rootError != null) Icons.Default.Warning else Icons.Default.Security, + contentDescription = "Root Status", + tint = when { + rootError != null -> MaterialTheme.colorScheme.onErrorContainer + isRootEnabled -> Color.White + isRootAvailable == true -> MaterialTheme.colorScheme.onPrimaryContainer + else -> MaterialTheme.colorScheme.onSurfaceVariant + } + ) + + Text( + text = when { + isRootEnabled -> "Root Access Active" + isRootAvailable == true -> "Root Access Available" + isRootAvailable == false -> "Root Access Not Available" + else -> "Checking Root Access..." + }, + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.Bold, + color = when { + rootError != null -> MaterialTheme.colorScheme.onErrorContainer + isRootEnabled -> Color.White + isRootAvailable == true -> MaterialTheme.colorScheme.onPrimaryContainer + else -> MaterialTheme.colorScheme.onSurfaceVariant + } + ) + } + + Text( + text = when { + rootError != null -> rootError + isRootEnabled -> "All system processes are visible with detailed information" + isRootAvailable == true -> "Enable root access to see all system processes and detailed information" + isRootAvailable == false -> "Device is not rooted or root access is denied. Only user processes will be shown" + else -> "Detecting root capabilities..." + }, + style = MaterialTheme.typography.bodyMedium, + color = when { + rootError != null -> MaterialTheme.colorScheme.onErrorContainer + isRootEnabled -> Color.White.copy(alpha = 0.9f) + isRootAvailable == true -> MaterialTheme.colorScheme.onPrimaryContainer + else -> MaterialTheme.colorScheme.onSurfaceVariant + } + ) + + Row( + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + if (isRootAvailable == true && !isRootEnabled) { + Button( + onClick = onRequestRoot, + colors = ButtonDefaults.buttonColors( + containerColor = MaterialTheme.colorScheme.primary + ) + ) { + Text("Enable Root Access") + } + } + + if (isRootEnabled) { + OutlinedButton( + onClick = onDisableRoot, + colors = ButtonDefaults.outlinedButtonColors( + contentColor = Color.White + ) + ) { + Text("Disable Root") + } + } + } + } + } +} + +@Composable +fun SystemInfoCard( + systemInfo: SystemInfo?, + modifier: Modifier = Modifier +) { + if (systemInfo == null) return + + Card( + modifier = modifier + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 8.dp), + colors = CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.secondaryContainer + ) + ) { + Column( + modifier = Modifier.padding(16.dp), + verticalArrangement = Arrangement.spacedBy(12.dp) + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + Icon( + imageVector = Icons.Default.Computer, + contentDescription = "System Info", + tint = MaterialTheme.colorScheme.onSecondaryContainer + ) + Text( + text = "System Information", + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.Bold, + color = MaterialTheme.colorScheme.onSecondaryContainer + ) + } + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + SystemMetricCard( + icon = Icons.Default.Speed, + label = "Load Avg", + value = "${String.format("%.2f", systemInfo.loadAverage.first)} " + + "${String.format("%.2f", systemInfo.loadAverage.second)} " + + "${String.format("%.2f", systemInfo.loadAverage.third)}", + modifier = Modifier.weight(1f) + ) + + SystemMetricCard( + icon = Icons.Default.Memory, + label = "Memory", + value = formatSystemMemory(systemInfo.memoryInfo.used, systemInfo.memoryInfo.total), + modifier = Modifier.weight(1f) + ) + } + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + SystemMetricCard( + icon = Icons.Default.Computer, + label = "Uptime", + value = formatUptime(systemInfo.uptime), + modifier = Modifier.weight(1f) + ) + + SystemMetricCard( + icon = Icons.Default.Security, + label = "Processes", + value = systemInfo.totalProcesses.toString(), + modifier = Modifier.weight(1f) + ) + } + + if (systemInfo.cpuInfo != "Unknown") { + Text( + text = "CPU: ${systemInfo.cpuInfo}", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSecondaryContainer.copy(alpha = 0.8f) + ) + } + } + } +} + +@Composable +fun SystemMetricCard( + icon: ImageVector, + label: String, + value: String, + modifier: Modifier = Modifier +) { + Card( + modifier = modifier, + colors = CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.surface.copy(alpha = 0.5f) + ) + ) { + Column( + modifier = Modifier.padding(8.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(4.dp) + ) { + Icon( + imageVector = icon, + contentDescription = label, + modifier = Modifier.size(16.dp), + tint = MaterialTheme.colorScheme.onSecondaryContainer + ) + Text( + text = label, + style = MaterialTheme.typography.labelSmall, + color = MaterialTheme.colorScheme.onSecondaryContainer.copy(alpha = 0.8f) + ) + Text( + text = value, + style = MaterialTheme.typography.bodySmall, + fontWeight = FontWeight.Bold, + color = MaterialTheme.colorScheme.onSecondaryContainer + ) + } + } +} + +@Composable +fun RootErrorDialog( + error: String, + onDismiss: () -> Unit +) { + AlertDialog( + onDismissRequest = onDismiss, + icon = { + Icon( + imageVector = Icons.Default.Warning, + contentDescription = "Error", + tint = MaterialTheme.colorScheme.error + ) + }, + title = { Text("Root Access Error") }, + text = { Text(error) }, + confirmButton = { + TextButton(onClick = onDismiss) { + Text("OK") + } + } + ) +} + +private fun formatSystemMemory(used: Long, total: Long): String { + if (total == 0L) return "N/A" + + val usedMB = used / (1024 * 1024) + val totalMB = total / (1024 * 1024) + val percentage = (used.toDouble() / total * 100).toInt() + + return when { + totalMB >= 1024 -> { + val usedGB = String.format("%.1f", usedMB / 1024.0) + val totalGB = String.format("%.1f", totalMB / 1024.0) + "$usedGB/$totalGB GB ($percentage%)" + } + else -> "$usedMB/$totalMB MB ($percentage%)" + } +} + +private fun formatUptime(uptimeMs: Long): String { + if (uptimeMs == 0L) return "Unknown" + + val duration = uptimeMs.milliseconds + val days = duration.inWholeDays + val hours = duration.inWholeHours % 24 + val minutes = duration.inWholeMinutes % 60 + + return when { + days > 0 -> "${days}d ${hours}h ${minutes}m" + hours > 0 -> "${hours}h ${minutes}m" + else -> "${minutes}m" + } +} diff --git a/app/src/main/java/com/manalejandro/topcommand/ui/screen/ProcessMonitorScreen.kt b/app/src/main/java/com/manalejandro/topcommand/ui/screen/ProcessMonitorScreen.kt new file mode 100644 index 0000000..092b288 --- /dev/null +++ b/app/src/main/java/com/manalejandro/topcommand/ui/screen/ProcessMonitorScreen.kt @@ -0,0 +1,366 @@ +package com.manalejandro.topcommand.ui.screen + +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Clear +import androidx.compose.material.icons.filled.Refresh +import androidx.compose.material.icons.filled.Search +import androidx.compose.material.icons.filled.Security +import androidx.compose.material.icons.filled.Settings +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.lifecycle.viewmodel.compose.viewModel +import com.manalejandro.topcommand.model.ProcessInfo +import com.manalejandro.topcommand.ui.components.ProcessItem +import com.manalejandro.topcommand.ui.components.SortHeader +import com.manalejandro.topcommand.ui.components.RootAccessCard +import com.manalejandro.topcommand.ui.components.SystemInfoCard +import com.manalejandro.topcommand.ui.components.RootErrorDialog +import com.manalejandro.topcommand.ui.components.ProcessDetailDialog +import com.manalejandro.topcommand.viewmodel.ProcessViewModel + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun ProcessMonitorScreen( + viewModel: ProcessViewModel = viewModel() +) { + val processes by viewModel.filteredProcesses.collectAsState() + val isLoading by viewModel.isLoading.collectAsState() + val searchQuery by viewModel.searchQuery.collectAsState() + val sortBy by viewModel.sortBy.collectAsState() + val isAscending by viewModel.isAscending.collectAsState() + val isAutoRefresh by viewModel.isAutoRefresh.collectAsState() + + // Root access states + val isRootAvailable by viewModel.isRootAvailable.collectAsState() + val isRootEnabled by viewModel.isRootEnabled.collectAsState() + val systemInfo by viewModel.systemInfo.collectAsState() + val rootError by viewModel.rootError.collectAsState() + + // Process details states + val showProcessDetails by viewModel.showProcessDetails.collectAsState() + val selectedProcessDetails by viewModel.selectedProcessDetails.collectAsState() + val isLoadingDetails by viewModel.isLoadingDetails.collectAsState() + val currentSelectedProcess = remember { mutableStateOf(null) } + + var showSettings by remember { mutableStateOf(false) } + + Scaffold( + topBar = { + TopAppBar( + title = { + Column { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + Text( + text = "Top Command", + style = MaterialTheme.typography.headlineSmall, + fontWeight = FontWeight.Bold + ) + if (isRootEnabled) { + Icon( + imageVector = Icons.Default.Security, + contentDescription = "Root Active", + modifier = Modifier.size(20.dp), + tint = MaterialTheme.colorScheme.primary + ) + } + } + Text( + text = if (isRootEnabled) + "${processes.size} processes (Root)" + else + "${processes.size} processes (User)", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + }, + actions = { + IconButton(onClick = { viewModel.loadProcesses() }) { + Icon( + imageVector = Icons.Default.Refresh, + contentDescription = "Refresh" + ) + } + IconButton(onClick = { showSettings = true }) { + Icon( + imageVector = Icons.Default.Settings, + contentDescription = "Settings" + ) + } + }, + colors = TopAppBarDefaults.topAppBarColors( + containerColor = MaterialTheme.colorScheme.primaryContainer, + titleContentColor = MaterialTheme.colorScheme.onPrimaryContainer + ) + ) + } + ) { paddingValues -> + Column( + modifier = Modifier + .fillMaxSize() + .padding(paddingValues) + ) { + // Root access card + RootAccessCard( + isRootAvailable = isRootAvailable, + isRootEnabled = isRootEnabled, + onRequestRoot = { viewModel.requestRootAccess() }, + onDisableRoot = { viewModel.disableRootAccess() }, + rootError = rootError + ) + + // System info card (only shown when root is enabled) + if (isRootEnabled) { + SystemInfoCard(systemInfo = systemInfo) + } + + // Search bar + SearchBar( + query = searchQuery, + onQueryChange = viewModel::updateSearchQuery, + modifier = Modifier.padding(16.dp) + ) + + // Sort header + SortHeader( + sortBy = sortBy, + isAscending = isAscending, + onSortChange = viewModel::updateSortBy + ) + + // Status indicators + StatusIndicators( + isLoading = isLoading, + isAutoRefresh = isAutoRefresh, + processCount = processes.size, + isRootEnabled = isRootEnabled, + modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp) + ) + + // Process list + if (isLoading && processes.isEmpty()) { + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center + ) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + CircularProgressIndicator() + Spacer(modifier = Modifier.height(16.dp)) + Text( + text = if (isRootEnabled) + "Loading all system processes..." + else + "Loading user processes..." + ) + } + } + } else { + LazyColumn( + modifier = Modifier.fillMaxSize(), + contentPadding = PaddingValues(bottom = 16.dp) + ) { + items( + items = processes, + key = { it.pid } + ) { process -> + ProcessItem( + process = process, + onClick = { + currentSelectedProcess.value = process + viewModel.showProcessDetails(process) + } + ) + } + } + } + } + + // Settings dialog + if (showSettings) { + SettingsDialog( + viewModel = viewModel, + onDismiss = { showSettings = false } + ) + } + + // Root error dialog + rootError?.let { error -> + RootErrorDialog( + error = error, + onDismiss = { viewModel.clearRootError() } + ) + } + + // Process detail dialog + if (showProcessDetails && currentSelectedProcess.value != null) { + ProcessDetailDialog( + process = currentSelectedProcess.value!!, + detailedInfo = selectedProcessDetails, + onDismiss = { viewModel.hideProcessDetails() }, + onRefreshDetails = { pid -> viewModel.loadProcessDetails(pid) }, + isLoadingDetails = isLoadingDetails + ) + } + } +} + +@Composable +fun SearchBar( + query: String, + onQueryChange: (String) -> Unit, + modifier: Modifier = Modifier +) { + OutlinedTextField( + value = query, + onValueChange = onQueryChange, + modifier = modifier.fillMaxWidth(), + placeholder = { Text("Search processes...") }, + leadingIcon = { + Icon( + imageVector = Icons.Default.Search, + contentDescription = "Search" + ) + }, + trailingIcon = { + if (query.isNotEmpty()) { + IconButton(onClick = { onQueryChange("") }) { + Icon( + imageVector = Icons.Default.Clear, + contentDescription = "Clear" + ) + } + } + }, + singleLine = true + ) +} + +@Composable +fun StatusIndicators( + isLoading: Boolean, + isAutoRefresh: Boolean, + processCount: Int, + isRootEnabled: Boolean, + modifier: Modifier = Modifier +) { + Row( + modifier = modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(12.dp) + ) { + if (isLoading) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(4.dp) + ) { + CircularProgressIndicator( + modifier = Modifier.size(16.dp), + strokeWidth = 2.dp + ) + Text( + text = "Updating...", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.primary + ) + } + } + + StatusChip( + text = if (isAutoRefresh) "Auto-refresh ON" else "Auto-refresh OFF", + isActive = isAutoRefresh + ) + } + + Text( + text = "$processCount processes found", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } +} + +@Composable +fun StatusChip( + text: String, + isActive: Boolean, + modifier: Modifier = Modifier +) { + AssistChip( + onClick = { /* Handle click if needed */ }, + label = { Text(text, style = MaterialTheme.typography.labelSmall) }, + modifier = modifier, + colors = AssistChipDefaults.assistChipColors( + containerColor = if (isActive) + MaterialTheme.colorScheme.primaryContainer + else + MaterialTheme.colorScheme.surfaceVariant, + labelColor = if (isActive) + MaterialTheme.colorScheme.onPrimaryContainer + else + MaterialTheme.colorScheme.onSurfaceVariant + ) + ) +} + +@Composable +fun SettingsDialog( + viewModel: ProcessViewModel, + onDismiss: () -> Unit +) { + val refreshInterval by viewModel.refreshInterval.collectAsState() + val isAutoRefresh by viewModel.isAutoRefresh.collectAsState() + + AlertDialog( + onDismissRequest = onDismiss, + title = { Text("Settings") }, + text = { + Column( + verticalArrangement = Arrangement.spacedBy(16.dp) + ) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Text("Auto-refresh") + Switch( + checked = isAutoRefresh, + onCheckedChange = { viewModel.toggleAutoRefresh() } + ) + } + + if (isAutoRefresh) { + Text("Refresh interval: ${refreshInterval / 1000}s") + Slider( + value = refreshInterval.toFloat(), + onValueChange = { viewModel.updateRefreshInterval(it.toLong()) }, + valueRange = 1000f..10000f, + steps = 8 + ) + } + } + }, + confirmButton = { + TextButton(onClick = onDismiss) { + Text("Done") + } + } + ) +} diff --git a/app/src/main/java/com/manalejandro/topcommand/ui/theme/Color.kt b/app/src/main/java/com/manalejandro/topcommand/ui/theme/Color.kt new file mode 100644 index 0000000..6cd1566 --- /dev/null +++ b/app/src/main/java/com/manalejandro/topcommand/ui/theme/Color.kt @@ -0,0 +1,28 @@ +package com.manalejandro.topcommand.ui.theme + +import androidx.compose.ui.graphics.Color + +// Primary colors - Terminal/Console inspired +val Primary80 = Color(0xFF00E676) // Bright green +val PrimaryContainer80 = Color(0xFF1B5E20) // Dark green +val Secondary80 = Color(0xFF81C784) // Light green +val SecondaryContainer80 = Color(0xFF2E7D32) // Medium green + +val Primary40 = Color(0xFF4CAF50) // Green +val PrimaryContainer40 = Color(0xFFC8E6C9) // Very light green +val Secondary40 = Color(0xFF388E3C) // Dark green +val SecondaryContainer40 = Color(0xFFE8F5E8) // Very light green + +// System colors +val Surface = Color(0xFF121212) // Dark surface +val SurfaceVariant = Color(0xFF1E1E1E) // Slightly lighter dark +val OnSurface = Color(0xFFE0E0E0) // Light text +val OnSurfaceVariant = Color(0xFFB0B0B0) // Medium light text + +// Status colors +val CpuHigh = Color(0xFFF44336) // Red +val CpuMedium = Color(0xFFFF9800) // Orange +val CpuLow = Color(0xFF4CAF50) // Green +val MemoryHigh = Color(0xFFE91E63) // Pink +val MemoryMedium = Color(0xFF9C27B0) // Purple +val MemoryLow = Color(0xFF2196F3) // Blue diff --git a/app/src/main/java/com/manalejandro/topcommand/ui/theme/Theme.kt b/app/src/main/java/com/manalejandro/topcommand/ui/theme/Theme.kt new file mode 100644 index 0000000..f2a80d9 --- /dev/null +++ b/app/src/main/java/com/manalejandro/topcommand/ui/theme/Theme.kt @@ -0,0 +1,84 @@ +package com.manalejandro.topcommand.ui.theme + +import android.app.Activity +import android.os.Build +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.darkColorScheme +import androidx.compose.material3.dynamicDarkColorScheme +import androidx.compose.material3.dynamicLightColorScheme +import androidx.compose.material3.lightColorScheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.SideEffect +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.toArgb +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalView +import androidx.core.view.WindowCompat + +private val DarkColorScheme = darkColorScheme( + primary = Primary80, + onPrimary = Color.Black, + primaryContainer = PrimaryContainer80, + onPrimaryContainer = Primary80, + secondary = Secondary80, + onSecondary = Color.Black, + secondaryContainer = SecondaryContainer80, + onSecondaryContainer = Secondary80, + surface = Surface, + onSurface = OnSurface, + surfaceVariant = SurfaceVariant, + onSurfaceVariant = OnSurfaceVariant, + background = Color(0xFF0F0F0F), + onBackground = OnSurface +) + +private val LightColorScheme = lightColorScheme( + primary = Primary40, + onPrimary = Color.White, + primaryContainer = PrimaryContainer40, + onPrimaryContainer = Primary40, + secondary = Secondary40, + onSecondary = Color.White, + secondaryContainer = SecondaryContainer40, + onSecondaryContainer = Secondary40, + surface = Color.White, + onSurface = Color.Black, + surfaceVariant = Color(0xFFF5F5F5), + onSurfaceVariant = Color(0xFF666666), + background = Color(0xFFFFFBFE), + onBackground = Color(0xFF1C1B1F) +) + +@Composable +fun TopCommandTheme( + darkTheme: Boolean = isSystemInDarkTheme(), + // Dynamic color is available on Android 12+ + dynamicColor: Boolean = false, // Disabled to use our custom theme + content: @Composable () -> Unit +) { + val colorScheme = when { + dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> { + val context = LocalContext.current + if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context) + } + + darkTheme -> DarkColorScheme + else -> LightColorScheme + } + + val view = LocalView.current + if (!view.isInEditMode) { + SideEffect { + val window = (view.context as Activity).window + window.statusBarColor = colorScheme.primary.toArgb() + WindowCompat.getInsetsController(window, view).isAppearanceLightStatusBars = !darkTheme + } + } + + MaterialTheme( + colorScheme = colorScheme, + typography = Typography, + content = content + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/manalejandro/topcommand/ui/theme/Type.kt b/app/src/main/java/com/manalejandro/topcommand/ui/theme/Type.kt new file mode 100644 index 0000000..f8dd6f0 --- /dev/null +++ b/app/src/main/java/com/manalejandro/topcommand/ui/theme/Type.kt @@ -0,0 +1,34 @@ +package com.manalejandro.topcommand.ui.theme + +import androidx.compose.material3.Typography +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.sp + +// Set of Material typography styles to start with +val Typography = Typography( + bodyLarge = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Normal, + fontSize = 16.sp, + lineHeight = 24.sp, + letterSpacing = 0.5.sp + ) + /* Other default text styles to override + titleLarge = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Normal, + fontSize = 22.sp, + lineHeight = 28.sp, + letterSpacing = 0.sp + ), + labelSmall = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Medium, + fontSize = 11.sp, + lineHeight = 16.sp, + letterSpacing = 0.5.sp + ) + */ +) \ No newline at end of file diff --git a/app/src/main/java/com/manalejandro/topcommand/viewmodel/ProcessViewModel.kt b/app/src/main/java/com/manalejandro/topcommand/viewmodel/ProcessViewModel.kt new file mode 100644 index 0000000..6fcd9ce --- /dev/null +++ b/app/src/main/java/com/manalejandro/topcommand/viewmodel/ProcessViewModel.kt @@ -0,0 +1,269 @@ +package com.manalejandro.topcommand.viewmodel + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.manalejandro.topcommand.model.ProcessInfo +import com.manalejandro.topcommand.model.SortBy +import com.manalejandro.topcommand.service.ProcessMonitorService +import com.manalejandro.topcommand.service.RootService +import com.manalejandro.topcommand.service.ProcessRootInfo +import com.manalejandro.topcommand.service.SystemInfo +import com.manalejandro.topcommand.service.ProcessDetailedInfo +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.launch + +class ProcessViewModel : ViewModel() { + + private val processService = ProcessMonitorService() + private val rootService = RootService() + + private val _processes = MutableStateFlow>(emptyList()) + val processes: StateFlow> = _processes.asStateFlow() + + private val _filteredProcesses = MutableStateFlow>(emptyList()) + val filteredProcesses: StateFlow> = _filteredProcesses.asStateFlow() + + private val _isLoading = MutableStateFlow(false) + val isLoading: StateFlow = _isLoading.asStateFlow() + + private val _searchQuery = MutableStateFlow("") + val searchQuery: StateFlow = _searchQuery.asStateFlow() + + private val _sortBy = MutableStateFlow(SortBy.CPU) + val sortBy: StateFlow = _sortBy.asStateFlow() + + private val _isAscending = MutableStateFlow(false) + val isAscending: StateFlow = _isAscending.asStateFlow() + + private val _refreshInterval = MutableStateFlow(2000L) + val refreshInterval: StateFlow = _refreshInterval.asStateFlow() + + private val _isAutoRefresh = MutableStateFlow(true) + val isAutoRefresh: StateFlow = _isAutoRefresh.asStateFlow() + + // Root access states + private val _isRootAvailable = MutableStateFlow(null) + val isRootAvailable: StateFlow = _isRootAvailable.asStateFlow() + + private val _isRootEnabled = MutableStateFlow(false) + val isRootEnabled: StateFlow = _isRootEnabled.asStateFlow() + + private val _systemInfo = MutableStateFlow(null) + val systemInfo: StateFlow = _systemInfo.asStateFlow() + + private val _rootError = MutableStateFlow(null) + val rootError: StateFlow = _rootError.asStateFlow() + + // Process details states + private val _selectedProcessDetails = MutableStateFlow(null) + val selectedProcessDetails: StateFlow = _selectedProcessDetails.asStateFlow() + + private val _isLoadingDetails = MutableStateFlow(false) + val isLoadingDetails: StateFlow = _isLoadingDetails.asStateFlow() + + private val _showProcessDetails = MutableStateFlow(false) + val showProcessDetails: StateFlow = _showProcessDetails.asStateFlow() + + init { + checkRootAvailability() + startAutoRefresh() + } + + private fun checkRootAvailability() { + viewModelScope.launch { + try { + val isAvailable = rootService.isRootAccessible() + _isRootAvailable.value = isAvailable + } catch (e: Exception) { + _isRootAvailable.value = false + } + } + } + + fun requestRootAccess() { + viewModelScope.launch { + _isLoading.value = true + _rootError.value = null + + try { + val success = rootService.requestRootAccess() + if (success) { + _isRootEnabled.value = true + loadSystemInfo() + loadProcesses() + } else { + _rootError.value = "Root access denied or not available" + } + } catch (e: Exception) { + _rootError.value = "Error requesting root access: ${e.message}" + } finally { + _isLoading.value = false + } + } + } + + fun disableRootAccess() { + viewModelScope.launch { + rootService.closeRootSession() + _isRootEnabled.value = false + _systemInfo.value = null + loadProcesses() // Reload with non-root method + } + } + + private fun loadSystemInfo() { + if (!_isRootEnabled.value) return + + viewModelScope.launch { + try { + val info = rootService.getSystemInfo() + _systemInfo.value = info + } catch (e: Exception) { + // System info is optional, don't show error + } + } + } + + fun loadProcesses() { + viewModelScope.launch { + _isLoading.value = true + try { + if (_isRootEnabled.value) { + loadRootProcesses() + } else { + loadNormalProcesses() + } + filterAndSortProcesses() + } catch (e: Exception) { + _rootError.value = "Error loading processes: ${e.message}" + } finally { + _isLoading.value = false + } + } + } + + private suspend fun loadRootProcesses() { + val rootProcesses = rootService.getAllProcessesWithRoot() + val processes = rootProcesses.map { rootProcess -> + ProcessInfo( + pid = rootProcess.pid, + name = rootProcess.name, + cpuUsage = rootProcess.cpuUsage, + memoryUsage = rootProcess.memoryUsage, + memoryPercentage = rootProcess.memoryPercentage, + user = rootProcess.user, + state = rootProcess.state, + priority = 0, // Not available from ps command + threads = 1 // Not available from ps command + ) + } + _processes.value = processes + } + + private suspend fun loadNormalProcesses() { + val processList = processService.getProcessList() + _processes.value = processList + } + + fun updateSearchQuery(query: String) { + _searchQuery.value = query + filterAndSortProcesses() + } + + fun updateSortBy(sortBy: SortBy) { + if (_sortBy.value == sortBy) { + _isAscending.value = !_isAscending.value + } else { + _sortBy.value = sortBy + _isAscending.value = false + } + filterAndSortProcesses() + } + + fun toggleAutoRefresh() { + _isAutoRefresh.value = !_isAutoRefresh.value + if (_isAutoRefresh.value) { + startAutoRefresh() + } + } + + fun updateRefreshInterval(intervalMs: Long) { + _refreshInterval.value = intervalMs + } + + private fun startAutoRefresh() { + viewModelScope.launch { + while (_isAutoRefresh.value) { + loadProcesses() + delay(_refreshInterval.value) + } + } + } + + private fun filterAndSortProcesses() { + val query = _searchQuery.value.lowercase() + val filtered = if (query.isEmpty()) { + _processes.value + } else { + _processes.value.filter { process -> + process.name.lowercase().contains(query) || + process.pid.toString().contains(query) || + process.user.lowercase().contains(query) + } + } + + val sorted = when (_sortBy.value) { + SortBy.PID -> filtered.sortedBy { it.pid } + SortBy.NAME -> filtered.sortedBy { it.name.lowercase() } + SortBy.CPU -> filtered.sortedBy { it.cpuUsage } + SortBy.MEMORY -> filtered.sortedBy { it.memoryUsage } + SortBy.USER -> filtered.sortedBy { it.user.lowercase() } + } + + _filteredProcesses.value = if (_isAscending.value) sorted else sorted.reversed() + } + + fun clearRootError() { + _rootError.value = null + } + + fun showProcessDetails(process: ProcessInfo) { + _showProcessDetails.value = true + loadProcessDetails(process.pid) + } + + fun hideProcessDetails() { + _showProcessDetails.value = false + _selectedProcessDetails.value = null + } + + fun loadProcessDetails(pid: Int) { + viewModelScope.launch { + _isLoadingDetails.value = true + try { + if (_isRootEnabled.value) { + val details = rootService.getProcessDetailedInfo(pid) + _selectedProcessDetails.value = details + } else { + // For non-root, we can still show basic details from /proc + val details = processService.getProcessBasicDetails(pid) + _selectedProcessDetails.value = details + } + } catch (e: Exception) { + _rootError.value = "Error loading process details: ${e.message}" + } finally { + _isLoadingDetails.value = false + } + } + } + + override fun onCleared() { + super.onCleared() + viewModelScope.launch { + rootService.closeRootSession() + } + } +} diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000..07d5da9 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_launcher_foreground.xml b/app/src/main/res/drawable/ic_launcher_foreground.xml new file mode 100644 index 0000000..2b068d1 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_foreground.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + \ 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 new file mode 100644 index 0000000..036d09b --- /dev/null +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file 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 new file mode 100644 index 0000000..036d09b --- /dev/null +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file 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 0000000000000000000000000000000000000000..efe2ea190666fd84fa164462fffc79b2af1d6975 GIT binary patch literal 984 zcmV;}11J1aNk&G{0{{S5MM6+kP&iD)0{{RoN5ByfZwEQDZPiMz`@Y~#1dPZ(3QL8V z>(?M0w2dT5PR;!X;rP?Mg%jDfY^B#Z|1%SklVSoF2*(Es#tS==ZPiMz``lkFfgDb8==^{4cnC^mvnL=9mhbjBX4*ty!U2&Ij%b5sI%`@0~U@rq>NirN>oaA%!xYH^%cyDIR_womb{g z>pxEOcb&a|XZq)~R!&_}(X6QWTwb#{FohCT&Z*)vM$iBEeTB4b+YG(g*tTms&c;q1 z8+A~(LEYF+ZQHhOH?`eg(d|2L_o+WUGZWGO2@w9iYqWx-O882Faw1OYHcOs%$q`wrqv;jiVtS0g#808mBrV&m*QUT;dut-`~ z3FJ6g>HisiVIe<-fUG48m$sz%g7eL9NJ_!HfO9auiOx|Vj#5HSu1LXiW&7Y zM?$N~-dG?qwru=VXmdxct@0J4=NF8DHljLn!^9~YW49tS|MEF$6UHSrp2&g5f^%0? zRv%s!xjX?HV}Y?T{Z|Z2UIg#k!E17t{v6n#$@C{7{m4CZciAfLh zh(4YW z(8t9=y{{q~YC3&9d_eMEdTJVqP&-ZY-p+0wvUhjxX{Ko_C~m21WbfF{sVyS5LF62f zBS+*Mks~5im@8x;~tElf>}WzW%qDpdub G@HZ7m|K2+Q literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..b9b6ff7abaf83eb39b9f5088bf59ac2661507a1f GIT binary patch literal 604 zcmV-i0;By>Nk&Fg0ssJ4MM6+kP&iCS0ssInp+G1QRX~y?Ic|ghe>x9ZpbZl~(Tza_ z6aWAWNpm*dRx+Dy+qP}nwv}w#wrwTwpw(_5D4)|VMm9&YW-(&u0F zvShW^Da?3oWlC%BrM=jni!e{`Cv15Ny)&cmUS}&@pN70!7eMvf&gC?8c!N|LB%xN# zDph693~$J?kgy{NF-b6pv5dbcWUS*HAP}fK2|;`KN`k>QrU9VES=VjQl0c{dh**TK zh+GyyG0zKTfFQ`!4jED03j%HHL@pO*^piv?-=LT%2yr!mw}zw=?B>`^458M8nYfTt zPK)0Ca&%&ea?j$hxH6mZJgK0fL3>MoG|F zwWA5Fq!SA}D-?1UZCf-U)C;NU)Q4N675O}()NzX(AdtrB6m@cjrNCs9LZD(3Yl6lG z!P0IY6ip!}giX|TKVqjHGO<+L4Nj^?!lIQ|cgJL_LO67V_w)+{R>9wNx&ZB3_j{dQ zA8uyvm*q{OrB>rDwV5oZ6m)zlm7OHJ>6As;RICMbclPP|>}VPDw;1jC q0^LeuoX_9ZF~baj{=R-;hPl8w@Cs+?*A6Se_v--)?B9PEi~<163LMn{ literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..c52506ed2ec9ded8a1c27512d5109486746e0a32 GIT binary patch literal 2786 zcmV<83LW)QNk&H63IG6CMM6+kP&iD^3IG5vN5Byf=1|bKjpXo$z1;&LA|{}n3Z=|e z82(gFmr|?$U%zVqb^h+|F1WjE{2$^^zTfW#%)kg_8-VIe!X^%pB}f)IGzMS{c3=W3 z>4OYniAvWA{s~DMnZQZ90S&q~xNebl&-uk0Z-62I00HN0+qN;=wr$(Coot)YO7{L? zWVitU5NvLXZQHhO+h%Rsnr!O`ckKTJ(2=%nk+pod6S=#)ySux)yF>Q7y9@66+st(1 zP7JBWY`|linV#wC4ZOuVSc9JhAd#)1ad$F`FQ*7KBXyYtKt`P!B5UFk;X2V!Md~qC z9liJloU;I2;}7=~eu*1*8Qi^7Tz1eOuCcpY+<29QNVZ*D={4uvPqcuECV2cvMwTu$sq;Iy#|QAkoq-RS}x5f%|YCnIe&Ox>Fkt zb?!Rkh0noV)3rdebO0#eT4tDg?Im_yFT|VT;cqcciE&eiM?ySXVHF{6-hJgYVp_N2 z7672>aZQL|LJG-@og$9USN;n?)dR)S07TbLV!R^8ZN|_Q zi7Q@G>Wst|Qi*Arc9c?TBC>T>+8aQ#0zwB6?~SK8{U}B-q|`>AZAuZ@M_U8v@@X0f z?zF`5A!kTU^s`A3*_#X~Uomv0 z7btrOnJ~Uy1Hv+dVHjd6##t7!F#zol{_7OXAnZ3CoL2*`U?B|95DM`$&y_&X{S*f0 zkywrt1D2vI#gHc?K-e!~a9sLxr0AUo28i$|l}@O&QQ*{C%N1hAFt2an1u1i;803Ut z2>U^Y0>tzRq$p7#Wn~;37ybVSKEH4VzP%9gi@6< zcK*|gCnw7T1A$(p9_0td9Wfarpk_8T;DWu2fio_?mfUdQY zc!0H8u9skAFyL0d^SA3V3#m9`S702U0)dNC7~ zgkqGumeME$k`Nkz4nyVZ7*!U1GF$U@LkF-Td5bquX~Diq?nf+8RRYuklb>=4F>nz9 z#fO4Ou>oiP5@4vR3Y9o#Ig$yC%CMu-3VcN+e|hyVw60|daEM75B2hj_cB%~9d{8y$ z!$95{*U*;1>~Htxo}Bw_PG^Xu)ase-0K5+h9^?#Z#}EhtFn z1ZQG2=qoJ;H5Rg^4*>#7WT?V^x0(iSi4FCg$uXLK<3oTU&aAn0(0maTl8AZV3zwO*=go+ETZCQd`d^i|DV-xtqV`XSh5=`f2@X4ALrX;Bnz645stRQQKYucTQNMw~MQ;sp%LTNcCCEAE$Xum$M1Kl)t!+wsN9;pv-2S zb}TPf-5`lBgMf73B{z{OhdNTE+W6t>>w!lrwoCQUkC~|yS1RE{GrBpzp0^wQW8D?d)5=|G1zpKrdwy6Q);UN9SWl<{sk47ke6kUQ)2&$Ei2@ zOh9LLLt60<0^+h{Ec(w(VMxk@1+VWV438BVAYW+D`n$(!xT*>}ptj`v*;vgh80- z*Ec5k@dZ-UR62*{*Bb7B11r{(Q_D{tL5}j^F_T7jn3ytkbR5k zhR;FN#;-@f-;sJ%r3q-1EMu2;(+@NAg7I?27vwXi-1t*TGWKuRt12A<{~NX25E4)! zVA|vD^!1eVMna?UzQjlA z=KRTxZffP6t;?F?;o@(v>UAm!hP&%eiwC o(0F_9b9Y(mzh7qGCIX5zB8>p8cA!_G#;n9SYqLK#-6Udw%*fY9NB{r; literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..83749be735d11d4c454cfca4e883a03542077042 GIT binary patch literal 676 zcmV;V0$cr3Nk>0ssJ4MM6+kP&iDF0ssInFTe{BX9OvdBuA3RYOO&W_v3Q^zBuP%svpO0F==h`Aa}`OptIX`V_dRm>MGd}?gfk?> zW6ZpQKei+Rpn?wcp@I%{Vf*1aTd-O39NN2evW1t!^JOzxvgO}dGc48OBq255_;n`l zE40J<*eKrjb&Si!HZ=cNs79AJs(}q_?60MSZn_PHG@%ch0h)m3Pcr~z^nf;R?G|dX z^snsx8%cqHIt3LZqmm?)1k{Rlky_CR88t!$D>{?V3KOj;2~tTCBFRL-L?9V8l3F3P z%Hj+{X9!FPw8fbODd+ViFZ}~C%PIeN}e@1TGMxrot-jhEFKP9t~ zI+@JL5Fo}#mw;GVT}2s30U6;i*|j?AxXpx2%4#Tt$l@a=W4flWtfZ7OR1CRsg8a^i zX$6I)3UwYsUMw2L;?+P@1RtDQyGXT?MSarV;{z$SN3Y>+ypFNPscm#K#uQHjwf5(p zCdB5x;Xa0lJ>GTzcQ+@}7zc6HStza3`(IJhJ(b()7pq8Dvt@$&^Q(@ck#?kK5An7lG#9==Fg-DxP~lbM@)v)veIXRW;DuR!Jp5?U86PGsfgIh&`pxiWD5; zJ?NKGt~VfM3@eiTrF+viBq=8|gHz?<*@?jlVKMs$g++u@<_%Tc#KrFl2&4{OOd#e+ KrB22l5&{5Tpi7DX literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..4e28a19ccc902852169a6ec879782c4df90b13c0 GIT binary patch literal 388 zcmV-~0ek*ZNk&F|0RRA3MM6+kP&iC)0RR9mYrq;1RX~m;Ns-k0EA5AU3stY9?3xI; z6#$TqV%xUWIh}2*(%H6cTU)rb2e}dQJ{z}<98vX!D)|ZdyFV}v=^NuX1>yPw+?>Bt zj|cyJ@HaNcLpdlKdN%h!U~py#^0`X`r5Z|r^ad>3IcGMc0hASeQHqoIeq*W0sNvgg z$nX>;cANPD2n3%Tp$kymwzVsow#(20s3XQeAqU*FUfbb;P0I*S2&;?3xoQk-C?(l46vu@F(HQ%ZWL~IYF%QGL?Hpv39 zYi;v>fpUogZn4k149%`Bx?bM{-@4q23*AsoQPgTTHnle#bPwKV#>3TPFVdnZNzS!Z icynX*tUJlDZa25eTOzYa66Woe*lQ#0wf+9tUjP9A611lP literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..17ab583924e9dbf72f8307b78227318d7b51cd3a GIT binary patch literal 1640 zcmV-u2ABC#Nk&Fs1^@t8MM6+kP&iCe1^@srFTe{B;vmSj+5EM?GepD$kS@JQI<~eg zwv>MeZ3D2dS5Lsw&fOx5-QC^YU9aDg)~M5NU=A!s(pK`H8o`VE?HOQa5w%p~L=H1> zhf}z_LtblrxU@vL{MpH{6S3;z6z=ll?oJUvY9vXLkk+g9BiQ_jo` zVtxK!_uhAoAakf@9JwcK53Xd}rmecRZ9YdfGTXMT%;pE!ez9#cXKY*NEC+7eIFgiX z@4J`W8{Aaewk-YY{ON6S004qb z>`b<8+qP|UvW;@;IsMK-003l@Z8mS)wr$(Ci#%2Tjkay$@S&+kz}7_plhGQtNf|-H zq`X?Xh|!LHVdt0d0Dz1VeQqYvT4{bda0dW@e@@I(vzi;~r^K1qt{vA@JXh&Pqo?-| zcqaMdXJ5uPaW$}mc8b-ed5zvQ0u8~+SB`#sUMm`KH6N~ot5b$fr-DdBR3XH|kceJC z(}nMHAc0xYRy*wpQVCJ7NU5#FDY?I(FP5+v&)sh$x6C7e`sZScVIz)Fy(6ma^dY z6LSCm;Hc+o)bsh<%|O3%hxgx7qhGc)oFkwt(UF2{#)m59i^!1r*8(>E`vzw`Wfu~7 zYHz(N51?fpda7p#jSvMP&8!H%#rw`pHkVUC%b5AIg3bQ_!4XM_Vx6Qk=aaQI)$IQ6 z-{+T__wL?%(lP^)i~r+u2V8C5NICA2ihpDxFzh8?7iTJi?66%-kVk3f2P|N5j&YeH zrHd#u6PgX(xx9_+{S{`yHMt0rbUE-1W4#Zmf~Ry4#SR@L()d}&D(140kBp005s?$n znp_FST$2nRE|4?MljjT`O`R=Wx9sblmK!MeMv*f_Izfhb>sev8^rwp%=gA@@XCNKX zmxrmcaJ59b=%TFly**BqdKjVu?h?cv!9D;Vv1N~ODI_f=>-2a(Qi8uqFN z2AAnn>Cqm=;867T$b809CAJlguQaf~8PKtwG6!$$kz@wV=J9u1fOY%%P;LwCM zdn{1lvw)wh>*M@Rz8#*cx&i)VsB)MEf`4^turi0jkn6i*@SGzOr*UjKp^j*d1f5KZ zQ&&qg?%iCW7pK1SKqp8SueEx#L&p-{1q<}4o2@waI~uuaodp(L_<)b!_D76BgvC(i&$ht1OV}_8gEYe#Aj0%s`YOa4cDz zPDPmVj0U&9C|vb-qf#u^{ay8+lnLB(SRUzE&fCIi!D|_sJNw^TAnbyPJ&fT_xaL zt3HxsK{H_a#9H|se;@fyuI;;QiUn(z|YoaK>}Dj|W%NJ`54yxHs3vS51iQ7*X5U%`U7 zKs)C3-h0WQmFv^kb7FTAa;buLPfX{}-`upz<16yPtDDaPg#{4{M&&y7Zv1-Xz2)P> zSMQD2o=;_0)A{`)7fjThpLo_M|Fd8mJ(S-uU}O8|l4OAy-Fsw_b}UP^>sq>Fw~OAq m_{e*sW0L+kh*9_GjC#bQk`J*!V}Y@L*N2ivU469uNdN#m14M!V literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..3f25650dac877bd6bc569f74e67b470a7470da4a GIT binary patch literal 1586 zcmV-22F>|WNk&F01^@t8MM6+kP&iB-1^@srU%(d-=1|bKk>m2Ged7m#h?oGRS{7ihb9nAStACG_j6lZ!8yoznxwq0gr0>)^vQl?LxDjAKf$~La=I{{Mj|6k{3|827s=%u`^}1$1~R?pdVBzIAoiX)8XMwx4}RqLP8-3AwY+_xBK*F`2Ttupxe zWim4mTZDpH>}*yC`=(7A6%nyTa7fkQT=&_Hl1~LtT0hWpd9UD6Q;^`0{=`BcrIgQ` zElep;FtfV<Cn9|6%54^l<)x{8$LWaZ3A1P=I=hde*l3yPwY*Y$pC^T~1 zhSlw|#z9v=IIZsBX!psr&B+O)c5>+sr8i#FIrfYMk1FW2WEv%b7!3!u!|4-@RkV3y zG7fjIFFF&i?yxyK#V8o6*>FL_!5vZ(M$RTnh-fhu;SPz-{RHKU&01`&Vhq9+7`a^h z=JW|H{&#r#1YiIpIg>QA`o1qdr83hMvlf}lO5;4wuBT@=B znTT#k$=G!|O&KI#Oh&IQGn(l93Hf70L`9{LZm~97?QD>o-T{N5TB9N?`eHLe7?_AY zzq7k}jD~7Rh2qaxS&PltC8HCF3X$Tb#ZXp_QpAAtFIwEBmgT4<%**`xqw(s_Q_m{8 zChtL_@mOWsr>ko%R%X4f@_RQRisV%?1tH*ds5 zA{5MMsPTj+y%9-ISiw@9mRX zip4US8y;v?L>$N2-M!T#^S4>s`W{}c0eHUb{7=S#Pdl&UPJL4!nLGBmg9%;kUK#9Cq%Xr@MY(-C|=A zLO>95@KDHEDs|QlrW`FW4$>$$GfDZxlBw?J3_Um56+Kj(yB2$_57D`c`7GE0F1W;2S5|} zxoBmJYWEJav98z2Zm-F1&&Qhn_ISMO?Q?naTEJ&xR!?#6Po&5J@P48qD}c@YBjL!a z#FMU(iN9)Erum;b$0JJt0Fc!Zc>n+a literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..f9146da98fe50dcc8a5a62ebb89866f7d2b821da GIT binary patch literal 870 zcmV-s1DX6%Nk&Fq0{{S5MM6+kP&iCc0{{Ro*T6LpRZx&5Np2(6|3B?>05nXSP1jtz zNE9L@K$2wI)*L+Bwr$(CZQHhOe%rQf+yB|N-#Ty`Ns(N;$IQ1-4@g_N{dfEC_TSG2 z!Pz-qeS7D$Z^carXT%=i@)%klkDo1$zCbgUO|Hy9Grl|<|n?eoqY@KVexj41KF7E(DPiex2mGQ}5C;^a_YBnu zM=uJ3%{p@b&{!r?5TBXS)l_hT$1JJ>xCL_ihXMM=6jK4>&yW_;7R1K}APf^p+~r2m zp)+y@5e0Cw*c&+5OD+YG?eOar{u8-Q6@;|WG(ersf)FuJk&);l?8fL8M50q2pz@+i zBpV=d8hzAVkod}}h2L+kDJ@7;nvja2ASn6kvmaRyzIM>IfVf4*1ql;(Z2A~0UhpqS z2wSub>*4>AFjG{L1S`4VDo8?DpME<6n)p8}*`yHUha168pB5N@i!_^<{y^7PuLNKy zNP?E7vv!0RB%%xwZeqF$3c;S6@ZE*++>BA{5czLl>O8~L#`HZ*OxX`>Tm`rt^c+n= zf^f~b>1Mhm!Xrmv1qpip__s4X7Nc(}lSGJ1{Y+;jW=z00LY-}B`Z*I5qES;c)X$zI zjQzA_UUpyeJMM?cUP$(4S=7Os*P1*!vQ@Ng&C0yAH!7&ntjtreQCGr6=Q&(1+^Vh9 zGq!CV^NgNOkh1%rKHY@JzL!)Z_zay7w_zYY7wx&Xiu%a;;2r80d#g6xGv=k;ICD(i wTHq)$ literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..2eb21359044e55740032b11ed945d921892771ab GIT binary patch literal 3950 zcmV-!50UUvNk&Fy4*&pHMM6+kP&iCl4*&o!U%(d->QKEStCH<(^C-;B%*+gmnVFdxE>s@Vg~#tQQ)8WTIk0X+hch4x~%6yGr%i*MaFTC4JghO zvetuAku6L20K!SH44@{E_D5bHpAke-s}odvU7dvkxGIblsxUMAmRE)&cW3~SwN?gE zVVJ>|VH&~-*~r${-ZCp*CA-zwjnqBAxV;? zM#_s|*mh`{Sxt8Lr1&9QCUwr$%szuLBK+gW!4 z)Wuo=iFScmMbA00(zMNr&~fQO77@#6`A{ooh<`~MJ9Gzc^Hgku6gfeTUQXkK=lpe4Jt_`Wvzqa_7e(x4^XS~935!;63I zYFb=>tGm7K0>3)?X#v1=L~J<%02C(;?GDq*;_F(694#5p5>OLhOpPI2#@JD{B>Qi# zz5@UVCw7DYPz*fvII%2wswJ(GF-nzWQjI6lX+o5Q^?jd^bUIBarHuXbmKT2qfDo}E z1VEXiftKvok}}B{CB#=_6^+>Q389p+AD#Xu07BqrIsk_yhFWq-OL`>pg7LWTQQbYi z-lOak=llkMV`An2l+mKH}l#N^JMgR^@W&=Rk9j0qZm1Jo`u_S5A zzNMbb0BA6=NeWOL6k7aTvU;Cl>(^6xum6EaVj=*v;AJZ7q zn8q}XX^b(ZF~*qI8@M@mC?rY~tmwwnfJ_KZ6a)xm{9Pt!_)Hks;gi{Xao6$n-TwBG z#*X*Tj?b^E#a$`Chyey7(F85n_;asH9E5I30407P(_RE>WmC)5ee?OP-6un>WK<~_ zh}WoFs_H1FxSQZ70W`VB%e^+o`3e;!C5%jt)1{!p6V6cNijq6#0M%rJq zc2Ki^D4?Yxu~0A`DV|4$&*iuzI0{Lx@A>A5E=h$FHl(p(7OfUII+!L`l4mkOqeUbU zG#89vz|3m|h-s$gK=5dqq@FH9db-U$wWM#ZsAascd5q|y_Q}$Lc`*x%5Kk6mVg6Hd z!HGyffulf;D;S}=h!`2M>YkGRIh~GEt7RwJ>Xdj-DW@ZZ5;izf6;Mh+M8r$ugOX6a z48}enJo|_sswR^q5%fr%s7aHwz}Lakz4Dre`{ZIb1zFhnGg@C85Sa$&eq=*q? zA+&$GbSECf5b%#w7@=4&Ai!)s-#1PgswR`;=B{~y z05?s5Bp@J)XMbXnw|k`ra~l1r)xL!CKt`=Eu~H$SBn`2;t^1FFfh0hZZ^e#V66I1H zm=K^sS{*6t(CfK)WCS(_{)2;$lJE$;@#W5-+)=X7p6d(OZz<*{0T6tDB1JH&#zM+3 z3KzACm|5dO2D-^IY_^Xp<+MMtj(}{NTk1GTWZ(L_op{sXqFg(Xp%S-JH+oD6C|uYo z;T5uw&|9JWX04`8DXVq*XGbf$BwRfBq>xmR!gVOQbsc$%4>dYvWfrzSY4AxdLkw5 z6iEiA2lgs(4H0iIx5Z&sD6dJx%;Zy&Lb**wvtXv1Ojzph%c55D$q8iHvtPG(h=WNj z?}-8BOQ;aDb8^A#hKF)-WjD2=(dwMUgn~i^%~_J1W;ec_LhZ|sNic530R9^cy*~n3 z^&>>6dQbnOECTJ9MvA64g)=z1-<}2^T+7+CDW1JQAEI zs@Ue4PW`tyCX$25-t~1`u``Na9Uy71!BSil<>CNKX1mzWlm3*dCkMkN?RT>e*6Z|K zB%$4=3Rh{Sv`+$H+6ef=W`C&(;BeiaR%5Y?^)kSK8=uGFWvs3*kX{?D=$a>oHP@Q| z2*h>(5CC&42t&Xfn7M}qaPZM90-5zgU+$|F4Ld7mx8$Po@r#?XB(PHBI#oSK!27)k z0N+NybWiPNo<{-w0ofLZgu(P>v_w^yg^pz>m;N@C zLqN@X)eJ^`9Fpb_O9jNhgn|Q^b(!e-a%dy$@4_V$145$Q0tmbHp8Yrm<1XrA1ZTp< zZ7&bGBZ^K&&y{@}dba1)`_|tsXJ!$|3eINW1^;5TeZRbMWgHw513GtY9s;UR|MN}$1Ksg@|fj&Mv|$m zJVepOB_0Q1UiDgAbK_Fb*vecZab@?q_D$OPTx%OX$Z+@<5omO&xgLzcHxO&VrgkL{ z7W*O(2R=M{YxW*P8rzs_9Il+du4B`-{-d|zg@Um8x@~y~v}Ue6Qxbu17}kQ72PwGv z{1z?qDIY9w|`C$vAV3OScR#><=Y@azIj{T zr2X}~vPJs3T-$NrRB`7N(dFYSwc95dLR{G|4SVc;)1*yX^LZqI09%oz7=dBuRd1e@ z(q+}}@#Mq%jOM}aVpjGzA`oGvS~@QMG+yK(V!m;A3q~LaGrM+kIRc|YzT{-3 z^dp!rA6u?Jx|DG95qe{_y?b66O#?@dnr)+%(;GT{H;T<8?c3uNTsa^O`+dLX$4t(G zu6zwZFCUZ$%w{Zq-c*|jZ~LZ>kFRi1E0-9%I|&GBsh-GUUt-1b^0F_f(qtJLO*SYO zQw+2{zGTocwGTPOqBo7l{xR1F?3G#bCn2z&ebvvFR3~qd3$U>QYJO>(98ZJu$i>D4SL7dWFBw**^^jTl5Bj-RTy+HHYZ(Td&4kc8lr*yx?@N`td)ed_KrSq?bzI0DYuts*{7|}M5m^D{cjz5u#4}_UEA`I zGs_&wSbA9*l9Sd$Zhp|>`|G=_@amd(JuS-tXC9xp0(04_`T7V<+t%~eA^Us5g-)xB zmTF;gLErT=mKBa=Wqj9PP|Bh#JoiKUz2M@y-#V$+oLNe_V2ThzT$d0+QVU8ct(N1?U0ZwpyHD{Q*Xri0Ke8&% zAqEj3*{as3*L-G*@7S*X&HZ=L+r~p&{CL)tS>5V7`ls2-XmP9GGT% zq}pd&nUy=+2VVM_ny0r`Wo?L2JBxt1^vX=Mrsf+oBd~hu6yI^)*lnYC*4w)Myx>f` z;ukY6|JQj{&b&31^VK!=NVWA!bzbm+-Rhbi)Ma(fOw0eZD|x=(7o6F7?`z+DyqAt@ z{^ZI`v@X4L)`l47aRej+?j7E>DxTq(#!+oke8+Bz?>J8J9oJR9xz9S^I(S33jo$Qa zVo&tLyR7mpyzlXd8@7#V9CN96-C@G;IENVLc7Ax(ia}JjIuq@ue{y_Hb7SL(5hEI# zYsP=_>3^*IVAb%v&A|}4oxk<|BMM6+kP&iCO2><{ukH8}k>QKa285FT0P7l@tHIP#PWw^T(vITrY$e;p3hE^=w zDrm#A?Sr8KJ0VU;5ftE6Ff_3>;Bwt7po6R7JEwuG5k>3}UB5Td>}C)U03g_Gw{6?D zZQHhO+qP}nwr$%s3t|HR1e@Dx+qP|cwr$(CZQHiJr2i8jr~W_3R{oEiGgZ#1%^9lH z=h)?(yPR{*neOHHc9#Afl+oUKXaD3dLRJGc6++N*TTGzgN+6X_#Fhx4L+kc!P*r`%k zBXFu@E?Fu6J}hmg|NY@kK$()%&I6;De`cT~OYvv7WYGEu#&$6jXQ#Dfh?_AM!T8~R zEyS-QjM)|b>un3llqr+O2rqM!)to#!mD97F>LhjL)KU09u3p>yLt2?n7~o}u_sHp1 zP7s15U?{}CFxJbw)Vxeun-qd1g5-phD3a3%TG7#hN$Sh!dZr<8uyw9)YOwsUDiBp#9)4?p7aQOvAp zHun?*uvXiy7i0Sdi9#{hNI5NLRS?ZQ=6uZr90~wVcP|l=ck!7~PBd{{Fet16@Va`6 zW7N=9$x+)AmqsU#35@EyEYDW~_&Avh!vv5J86H_hFHkWE8K|P+i4ajdP#`hngw*iT zo`QGJ#s2@9SoHTd=klJjanuPy+|9jx!f`E%{`@kq*xw(Q;ulsbfQAx5gn*3eo#6^S zzqwi^NYHUQ?F(RFY9>)k+eAtWJ7o_MEQRz%zP{Hx>VypkBSnbFgpp+~rhQTfkHEm) zF_p`EaKVR!fTqsNCpWyjk5a=Xr2}V^n4;f5xwnpG1XK+CW2?CAGn=5x2LstDxjkIc zDdmJTTDEeIA|GD`t{;?C9Gr@OiJTOh=w_QVo`JUVPNFz!hl+mv6u5cJv9P1n8uVsSz{Ynhm(H9pOq9O4Ax+<5YUDdb;Y!f z2PW6KJEghVr})QIMYoJa)el*T82J!jR$%h-%870owNv&SGXM~yVl0IXk#&Qx<^#>fe!`@$!T@7PqvY6UGhq4J~od3=nR(UqqRsUN}^A{B$KH zJ20uPPm;EgH3P4)C^)-?Z~`buSaFw{MFJP|I4{qfcJ}IiP&%7LH^SY&+TtdJLqxZZ z{|cXg0(Xa0H@jpFyNt?O%IX(cfpzJ8a1d_BklZ$NUNsyvgT5ifwhFHQsb0C+B%*6X z<-l;<^q!vCE*5dfh-sg=;U85YLBl06#db{2fvvp5Ke`gQLBl5NGFk5;0_>V02Vs`M2s1homHD}6}^6_Y4& zbVWhRga8_5tLXkOs$pbUL}kB&c2I|mgrTL}JEs}M!;C!|R%sSsFzBIIQgWi3M(;;A zkNE@^8qve^`2P83olQog?rnF6l=(aOkl=Zd)dLNO6Ayik$Mw!$il`n4FYn!%V}3jT z0__zt;IkKJ~dHjW-YpYkaFkSph+wn zX6AHF51Kq-2f>-m4RoNi6xL^+B$8iv88?VdP%u6@5w5GkXhR;mOlqPx;^&1|Q`= zzb*Fnf3K-NS;NgZioAbv zdH2s1qzqty1uw5~46QH+WYC|_{r_-#M^xPq1GoPzM#Jh!q3fIHd_Q2~2{2JPlymWy zu3nQd&Ec6$F9;q$k=?*Eqx@}c3?h4D-VZAH&Z-}{nHPLc!6S*c&58 zZ;X1*SC4KtX!ro)EJkL4xBWftOe%%*e)k5LAPQ0z>EYa`f->r%vZvZ$7Bzc}9^N9)Lc*z8?_Da-%|vyF2dH z`_0e^{>PWJCYO5>kNk&E-1pok7MM6+kP&iBv1pojqL%~oGmC%xHTW;HM{*zuGpusRi6<%5# ziel$OkrA>005qg%)|hNAviq`a+qT`=wlCYZZM(CXZOog1Z2sT;zxjXj|K|VA{~K4) zNiX4R-GRj#a0o2nFw6OMX?tH?&b9S@ z2OSx=aH82~auZub;~h(3Vqw3;{1s?z8!C^WWybF+<%`O}cwKEBMUC$jSQ0Lkweug7vGLIT)+b>H&`w zbgOn5<`0DNyxPCV308ww`q_((ryM1hi@QwVCxP}1a2;xxNH5P!aZ0vYTc%^S^}$8h z9W>Qg`lEz=)j>}f`&>2ja3oL!#G0;goRf>+-i;E?w(%tiOGg&pJLs1Pt~s^vJ4plp zyvGO$$8vX25~B7klM8W-`w}tL1Cs>VccWv(1qg+S%KI~n?D6|m#8-Hdh`GOph#{Yo zq= znWf4BY7%f=A0d$j!I=cnUq3LyN_BG%1;<|Rpd|cCZ-_9e;3VPVNW(RANn7?8Wun_} z@DM@r!AImI@d{9bG=q$PFSbcS%c;~1kqE5B$xY_n7oqTbP+YL@fK`$}X#^M!5-1%u zsbtF9ulk`_+*P8^GhiBR2Th+Xok0>xcFbd)1P3>54%+&mkUGK+n$oljl*1^AAc!&S zlO%71=My&j>DqB-xB6g4NnGod;=b8z?AT;TKzqG_8YNIX?O2z9cmPJZ5Fe=Qpn!Fi z|0qdF8V1Zs#5;qO+gAI7B}u>r9I+lIf#q!hu&c#=9yBkP>(iY5ww!j*UNV*}q79Se zMqjdbuTE{7m9O1?wcYmQZPhoFaWW-HeTtX_pDIAuzVLT)K^Q8tuRo5s?~8MFW7LF@ z9~O!4gEk1@95o?o({GY{!VusxY{K5D4x~LqMovUm(;nwaY2d(0ai;v7(6#3a{J;sc z3D2uXIU&F=gT0lJDTHrn$oNMd*Q67*gU`$}{^ShtNQG9Rf-0Bk4))~WN zL`M(*;eVlgVPf9$D#3EG~$}YgRj0tO19M@OVGL aS@r0om++O%|C|3e|8M@^{J;5syC4AI5RZre literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..6a0d4e241c12b8886605fae25ad976643f52a806 GIT binary patch literal 6024 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 CBA!G5 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..7d38b5a7e4ab237c1fc572fc7a93d086c3aacaf3 GIT binary patch literal 3082 zcmV+l4E6I;Nk&Ej3;+OEMM6+kP&iEW3jhEwzrZgL;!x1Gk(2O;eLrpih=>Vbpp8j; z=bWKQGTZ03Z*xm_G@B?1Dv&gu3_><#ZQBk>()piGM%vjf+qTWNGU|#h{B2vgYWL2z zZLDnD_R{XRZTHx@8gpkXZDqw3HAal*g2Mm+!AA9L+qP}nwr$(CZQHhO+qmomVgmpK zo7-yJwrzX1ZQHhO+qS)=|Bl>9Ql#+Wjj6k59zf~(|I>E!OWN2T7c(2P&fKAG+qP}n z?m2s}+Zo<~rhOxJ8~^Uw-@5(RGn<#QnDrUej%wTHF5{fF_N{GOG5;CaI-}m;U+=Z+ zHckd`BFPr5^qqY-|1N&?U7-Z42q2k5^^zza6gQCqH5=)FlF2kw`O5Ple~A6eHUl8F{!^r)oy{;6#q3hwTX zoqB{)dNgU#qnO7P9odA&VGa`-0d-bZSZn2qW<)Bh1#*oeTO{8#nE_p*TVNL+?BOaGX~``1n56otz$rWw}_qs3%kzE8lK z$}t+3p-nRbeCDIj+dpPr}JX z3J3~p9^|;x=Vu463>gs_>Fhr5g^qJp3k=k(!Xn>4T;b0zE7J%H z07AMu|NYiZ`RW)rdS!B4Xy23w8IDvU#lb15m^3UHdFDpPX$%1c1p5@Ua-5@gCc|eo zxNE%d*EdbI$zY_Q<6M0(0zxn(pnibk?A=nqjFwk!p&z8_1ZG|4I2V=1p-fc>NrVJj z_v;PoeZPY_=c}QiX6CQ|^8shm|9@=!`w7Oh;CK%jYkXdid~;jq<=xRQTgRtJN6)so zR*O6ZHVq08fk;Qw?;%lMIZa4ce^>Rt9mPx+LZBifxXDC;z~t@)9lydQtT3o$IBaAt zY#>x7V>MB~cdqPF>^{sPoz8Dor+LYAH$jRlTOUis($E4gM8X*)4;JxUjP1mRr3Jf7>6cF_FE;$hy z5FsP%oRXrKwxVX&{HuC=3R{C(hohIb8<}Z(cp$f!HgGj$cMVNX08Xyg@yWGZ((sXa4Tl(dZ0!@G=GK~{ zmwQ5mXGT3vZx}S}B20IUE?LI=mbB+rvV-jl=gTclX)1C-h*!5w4sCx-SmgJ*1?g4qqE^ofw|3r7rMSe z)_B*5;)LV#PSXLHRJ81IpHBzS%f>pkM*RNCb7mSIqW*7c?;2Th&Dt`0Kjr9M3s{rhj-41gFN%+$u-vy9g!FExMjw1HBF&o@58q_xOt^fcOp&$cl``&qh z+~eLKhV)LwtnGT|HHY*~ZLCG|art61x zg%*w2^{vBbG_hS&D@@$c{KdREx+2iYtMun5EiwhJBaWR*1BX~V^E z#_**IF-iMd0*6Y8fZa?oARrMiI+BqRSdC~_bVg8RID4xmm~l54z@X;V4y0utAIE}# z9tQ-`B&`UXChah?a2{M~OXZ8P^t!Xn!YEtK=D575%|Vz1R03{vDJ6kDf#<|$zc^nG zR!$?l?!_QBF21;KcrF`SwlwYK)d=K*h$YGbYi%a>5mu}5_k==}| z3u!j9FRDbKd@l!qYh>_>LHWj^{1Z|E38_GX=kAYVP(Egn9PeeWbdlX`n@eGAX&Mm- z2&8`#*bvygl!L(8@X7Jgl|TLM32T%lna(vW0aO%~ochE(R0_?kz-A(6%G>>am^sY2%Y=b9Fn!AcNJI<6z Ydq!L^&Eo*XG?UK!FA_k+E?h(a0PK|jGXMYp literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..5216ab0b6e3b4f8ddb4c1ccad930c99d08878736 GIT binary patch literal 1774 zcmV_jt{CJ4Z0jc zt>dP`&49BlA|wC+ST<5UduQ9WZQFd?wr$(CZL`mvZF^ob0k!#m^Z(}m&HtPKH~(+` z-~7M%fAjz5|IPpZ9?r))S6BR6-3iBd^%TIW-yg#KFYx*5NQx-(CEhRHNd7%v z?!!>m!~@9LU>eEn zKg_e)FOw(3-XkDWR}awHqHCM?dYnytCK1rtN}3h3Z#6jn(f@xNu6-`7_UE@x*_9Qq zKIO!kJvx^AR-9{ccpT!;}Ra45=t{M z1_hCi)G^=?y_)%JiszKk$YV(G_9bMtF{>p_(Gp#j3#S#2jRK7^tAXxL+(s)bFHw?C0-Hxg&ZKN5rm^U%AAvy*}!L?ylNUFF|*mZ#Bn-{klh#0{e0hoRR!W-ts`YPEGxPVx| zPxMwkBY<9xJSX<)pt}G={UM$}d>j#;s;DJ3x5}AOLa2Kz;Ho`h&BS;Lo(sIA8fyae z9VK-Hl>0}_jn7rGBX0rY<5o=(F<67Zu?D>uWnGv$k5l9Mp$_1$imp94Go%{} z?*?llVLJ$Hiz|&1VHjl#q~=PeinZW|>G1(F2>8Yrr$*V9ST{%7Bfb$;Q+q6^K~S#5 z9dP67aN;({Q!uPU#RvRq{)Or_40w`9PNNW(l7-Yoj))12mYphsa*m*y#Ogy)!=NmR zUOtVgKW~yyIdi|?wIA}r0A-OLDQKOLx&T~Ylsv*zkyJaQ6#oQ21(JjzyRv= z@l_hMM#;Z=^WWyb&3{`P+*_x2&m!l!Mt`rK6<QYkG)S7-gC8I)u~p$&p|jY2pJh*OMK0O@89dNz;rVU?|-AhjU<%hvn$DUPza;} z;xyBmeR(H0fn!2(f<&tY-}mHiOgNKRSBqYg`@?_Y1iewVJvnUk_wdW?tomsbMay>w2`O1`}x~~rntIk6f zSr-1iorhJ8z5kfo&_U-cN{;GfzJKa0>3kaR{)4T55*2jMq98veFZi?9Szc9Ss{h`9 z%Bn_?^v>cRyx-HjRR`YnG=j2)-+H~?pEULCJ*X5>H4>WVYTwfSy;L8TPtjWc`(C~H za{n6cm+Hr_=U4E2RZqq-UOmOS-5?Nk&GfAOHYYMM6+kP&iDRAOHX_zrZgL>QLOaZ6xnMcJ8G8N5lm1OND)I zih!5PZBTv%ePB2*w~Ow+wA_wRZLN5o|6e7^&PisL880(4Gcz;OEHex&@4&(^-QC&2 zWf-1@s?*mDZ@}-#x~EQ`sycPfi3~@&Wac)kE=DV-j%E@wyVsCijVfMEW~Nc4v)Q)U zZgXs@4G~v%k~ULhsX{ZeyA3I`-Hw<&+MAjgdM%4_q#o;-sn-l`%%wDRbBfo-@bEYGT*t-o}$4xM31H#4;~ny}5x6`GmdCz$j` z2->!7D06?>*0wiotnWm)r&q__4zX#ng+K@p=>Jn@b~~K=*_SQ>+qScYiz#Adm>C<`I>v?_E){WITyp-wu_+{O zWSGH^9v$YWEnr+wXN$NpXWUk>dJ6c4Ck2cS=*ZELaSSRhCtXC13v&%oIip7IKuJNF z23ALs%c!s;$u&gcoKtU7C3yicvVJzrjrM7ZfV*ysZ9s#xdL&&>I@`CO4rG#U@?ZsgM?=w>Rc)*=r z)DxV^S)I=v-$oCta@1rs7FcOi9CAOF0eyJ~yswymea@8n+yVQ%4j;dV$8j*ugK-Uv z+hB}KF#%%}jLk3x5Muc6e>?U+de!@Vv3))7hrj3#pFWKWYk`qk6l;O3(qZOV0ewjW zMq^;S3r3G%oCV_s0|*pS3|IlsVR*eac!raK$D`rAauRGrLV`^g5h)9ffsq-Y;s)>cPZxPvBl@RIehOs!;r>Gg|`j(I@GF`pJ{GHsbr?0K7^%{w-4s999mm6vxN}>G=24 z{IJY!Bx`N0^l?-GzWI9Sx>5ux2{D5M0KDG0-K&<2rHxja?zaeh`~v_G5?E1{5Cj0Y zphf!8wADzqI)~6-+yJ~7#u+j{C-5o`AyQ^PQz=?J5RjG0N}p<^qy(jN_15^o*sb90gzXRNM!(c@b@INSU}U; zi#-DHV*nOoc*2Ir0g&<>i+DEaXaS77K%Wqv6d`5+Dem&;7ZS~7rHwE;j-`@9 zwEOE%Y^gs~+DfS2CTlG{dIi8@#Kr8sD0TK4 zF;SYBYtD}tw@_^X?e{1e#*N^=?L7HHf(YQtH_rClKq6dM zYosNk5issXby7Az)ZKi|7}}^+`5X!s{^QaArobghbVVN?pMA{)m#U{$m;ZRU@|^(= zy^8)9>DE@?J=1doT~?KLpPUZH^-6a(0zB9^SN;5oDeZk9=0%ikd~01pqfCGb zo0UEv7?&trjSlwCA%6P5Kh4`m=I%+x))6Ir*B(w8X~%TE$`+F#Xr(J_kd@G?Y6W!j zTE29Yto(&+7zNsoFO1httm?DpQ81yvKuI6ApRbqwtdD|Mx;5w0s&%nb(Lc1e$) zD}NytEHoyw5o6uYZ)AKG7YzfH^#sTEB){@2$9rnLlF`SPhgq+A4FxJ~RK*2pmjoRF zh7ev!#8m98y;F?S7PmR5XrQp+12DelykhSF4DIwVllrua)~cd{|9Cb423}blcvWh^ zE7pB|``aZlzyU?(B&>^wtBOphHfoNRBAajLPw^(N6n_t9Z>@tS>Lo6#3c63dFO1v4 zV!YCL>I8Maf4J36(OL8<#os(N+)5)HhvPTs=KpexnI86NMn!TX4{D)ht&JJT=!Gc~>c_0)2QY zfzR+mFtfV0N`<$pN}h(}wV*(epnIT?9Z&c;L>p$IEsKbfh8|dw_&#~f&OX)ATKp>d z&rbv(h{q8&A|k$|GyC`=`}mTQ-sFdF1WyFMIR_19X1lDoX)Q7iFfK$iJmI2a2jErG z*Y;JDt0mq=UjDCK$>rh+pRwN=)FqRZxZx!ov&A2s0E^jiW|idT5dkrkhfOH)-lq5E z;|$NW(m`Y9lI56j(%MD-p9Ks&&NMmjtAT=U;AQbf^hYp@rl)3kq^oH;9gjUOi{$7v zqTx-}!P!tM+}p+B4WE(Mb^769PY*7V4~=X;J>}Z*-N-BGPdj zGEx(KMu(=WQa0TO$7NKOFR=KI<{%?gAxJUIu7x^^m6j^S45h~^#cT;q1B>xg1dJbBW2n;df+HGbhEUc z8;guohQQCPfvP*tvUF4Gp5ygDdWVcu2H&TR9;UtWI}{i$tupyJa>4fnq}W7li0q7N zqEd38GDVJA=gXf1i}BP3zu4~T(zkWTtVF(Hg_{&t0*>n7UaBcuJ61=LWz=%I!syzp zUjTqWbqMxZwNRB>uT#`)v_OIGfAkS?lM|fH4X*eJ1;k=hbeyGDFU1{zp+1-{&D33! zl!og%D(LpM<2L~VPkr!)IkZqie$cC!xk{i|*46^?Kl%wGsSqx2nky4l*d5n$b&9Q< zRr?}qDnw+Pv0W94qZC_yfwzr*4S=y(phAR@SDwmckM|1{TATdNzX2Rih472*x&9U3 zZHg?r>FQT_OHdV6!WA`Quu(fIbsZI1&pI~_0FfFI?K7!i(q;3!@}Ir5>&??lZTUmM zz*8f6=K%70{p0?YUOHxNmYuv}if({WH>>waBs##=olKr&SM1J|XY5*x8l z?UkG8=$I)x&;SIgL;y3erV7QA99t@8$idao)Ka$&$xtP%;m)c!?>BXH$+_#SH_$w7 z_18fpb;7mOL5;gS_1wYL$-%pl9yq?||57R)>Rg_(%&x4xa`<&exe_R#lPv|RBj4xzS0EAvF(oT? zVS9j+r~$}h!^49-Gbap>F1I3vXYy*F{tpGNW9OK)Oha_b^PNuu{@6qi`1s_ch9TAX ziVY112s|vjv$!$9IkmpN+KydP@UJ!>)9Adrhaq*S@!_Ekojq_w8S?VawD+%(+S2Ec-WPM5KD=qRW` zj99eY($<~6>m>9?l|;dgO;Qu!lnMaP#uawt-91jy*bFoehv2Cjl6Fkw z-9GqQ#hC$`Wj#}pwlUPO(=oLhZ=(r&R~HtZiAt6JxPc&;qB9%&)Dj*VO);iF$Uo6Jyz_e8?)k{ztLEz((vX3tV z-O`CsUV5P>cU5;obXvW;ss{|>prE5t>#~n7gd{Woi=<^FGO?x(DOvVLOh=hE5jHtY zS?2#)c4F}GtwJIyfRL1)w!Ay6J0u`Kv1WjeO9aFCIE0j`waCP3VyvH4ylQH#FJhXq zy(6}Z!DhcuK&x~P`Zt0Q7FL`xy>0<;mA9(_uLp@JS&HF>6-Z{10LPS+*^Pq$>^bFX zz?%&%7cHO}+gGOjApMqFAXt=bf#|EHLZyfx2K{2Po8wEnS+FpGkf4Giz*8%bP~O9W zhJbOl?2^5Y0eD;Y`|GCUL`+i^Ico1i^`kaNF@OT)we?e$g-UThF|hWDFX=J?$opp{ z?c)#;TjB4JOp|EHIluPM@Bvir;$s|zn5HZ-QoEu0Z?<5tU(IC65Gm><2qzVgw2hA; zGO5PTIvz-HkdZlO*NYQF2Y4GrBBrSd-&VUJntfxjZ_Om7fl}N}ltqECu#(i-4e&9G ziOFecBVo%X+1r6%1;9l^=RZETAU324+q;dn12mgzj$$)mi9w2531SHe$WNW$S{Yz; z>v=Xb+|-3F5hcqQ6A~2coqe@{hzhV^{=?IGh-u8UZ>@C{HtTJ}(u`B6mY_O73|wWb z-(LbCB&NO;7GCnq17Q)Rbw;ioaB~~b0Zu8Z$<rp>hq(fk}X+iWZL>&ZSmKa&9g1n^Mv-nt&3#gB)& zmlv+y7KEz?$-bx%`J`6KebR8dMe(Tr{f)3{0SaLczB6^}{sY3k3@-=7oaIEb3##dEem4m(rGZT%^oEo+;QTC-3HA*-`1syQ;}m5jhjBdxPGS zNN((OUQBcw>y8Tr>xRra0=;ijcE+sXk!3b?Jla@Qi@a}k?*PXXCIk>f!O4(f+O*?3Sm)WW7DIp}?;>H5J&2^S;?|Z=;CxmEH8!z3p0-xy)iIv!Ug= zt9hjD;}6ZWDH9qDKzHsEU)oiL(!JUAta)_dvZU+7X4O4gLcwaBj^PsAL8$jF5AwH9 zQjRX=Uft!KT`R|zci#Es-0Qo%yT>Zs&kvr)U=3+m-3}g}MeM{Q9N=12j`TzFZ^0iiW7Bh~UGQB3I;Q&#apH0H|)l3K= zGK%V6UbChiHNi`7j#^waMyVABW(*XZT3!E>xbNFfpSgV`X&HSRh;E5aZ%E%bxC@zk z`@AXck_-j+nB@kRg~r0PPbfIdrnyGo&mEk^3>h1TT?Tir0(|m-E+!-h6?1Sf*(J|P z_G!Y#vHWg%P_Um)xgG=wM8)i`=ACqy!4o24E0gL6tI!}p=8zfHFrJS?5$Ec_#O0kg zbCR+V7iJd;`?*6^kXP227n%XSNNyZHu?-C(Xvh%TXg@P^M_v~;w*&?2jkZwvqEe7W z+|Nx+QK|LWr&qp~aTaTkclNy=S^-4fJ2U&OeY$#WJq2~0R}Q*m1|+Ar)pef;{ki$d z5iu2GW^5iFzVQPMsJu6CrGv^R|3kqeXXydeVPk;;^C0GCszXSEz@J-LNBfvX`=5C@H-aoGMD#;YSJ}GJ z<|CZT?wW5xobnRG1a<-xJZHQ13y7pZSZ;R}caDRCPr{|hGV&5oK!$H>7hj9s!UhS{ zhY-?pbm6i1zp!~-a?ynyBz%i)BHHR3Ko<2O?zfJjWdpqp3tvVvn=qYRXp3pcz>R={ z*9>Rh@FL~GGIMBQvZX(BBZde(6S9`SgaW#}CJv#M$mK;qq&h@ssc+WaF*5BIc~ekvqc7~!!SJy zWkq-6?mtzwR%kg~Q~UU>wHee#-ws4-gV}FgMdyZl85cin6Nf-+1vU>JtOg9uP!Kda z^Rg&|r83xzSu`;5(yz($p8;yOB1;Dp*uyk&OdGv$dKG|ND!{X?dFg4lHTc z+xJdR8E04~uPW8N?w-cvB4c2;!iDoQhim8!1#xBB$rc<0y*%FMu4vvq(w2AQN;;!6 z8cVo+eDfZD1!shyh761*ypla^1E0)%bY-Q|!2>jM$sH)6Zg6ESe7{+$rCTwyp#A#3>7qr9+RIyh(uw}O zGT@`tp!zo5a&h7?4TKS?CS=5+d2Ko7K^lmTV6vrWuOLfMDZgl5KMogBFlmxW`ez>c zVE>^~1ZIKJ_i_CQUdfz;@xY72mM3cu1y!xf|1{?yl2s+2=)QlJ1=23#Lw-}0u-ptR zOt2&g3hMU#H}u5Zqx_bbl0h2?64%;&Oan#LHAkxQ+(H*=i}vG7(mqc5mig4^>*=V> zxrKOqOjwz$U2V5R5^tg;Mz#l^upmCN=mYvlN+CcWx>R20B-Luul$2 z*0X<1)t#FfBs7?Tv3GiL?qAb?@QO&q)Nm>233mQe6cp4`-R1kt8pwKivY%TkFiliY zVaY!zXk>Rx4^k0Nkh^D6_#h3$N3e{46;R_;>ML)*0Ljk{=DuQLY4+0JEaV7oIy_0Q zn5OIl@J`c*Dv&@yzNYsaQ}gnoIk(?PeQq=N(UZ!`R<*~$pD=%$u&=5y(4*&26x;Q z$%YN>ZTh)?YMK)YJewCo!?}Ph#~C+7f#TdZw)w=@z5;(_6v&9cB~~P{Q#$w0jC1DL zuq)tR+#L6*f3vR5M^&ea68(^ge2onfdrm4X{g=NPwm=nU4pd!rExgke{>`f1easXm zQ@_HGH;ZKJjIi*8lFXI7yFq_5Y(m{BJV}qLuCiv*K*2h^(+~y9zZv$80xMqjqp=En zc!(rd=k`18N*I6f-(lY<_-}?NP<0CLszG(tHJz*>8%lklD=&V~Knd4Os>bEjnTD|z zJ#(M?mtiWKjjKcU1~6^A0Ct&;4!V%z?Vf zTar1gbb+jNP{wZ6c4;-L@+;xF(>`N& zSt2pJftly*CX!@q+;Wk+*Fx>n%w6-tP@OF&+NG3%ov-TOA@F=ev&gZxpa^Zp3ug+H@d~# ziI)7VyMjp6a}0rIT$hPAj*H$w%7A~YRl5gyqu?#tY$&(* z<5Vu#0w0}cH!!jku5}H-hleZpj9V$Y=;P*WHvXeq{Q)UEh>k0cknIYj5`E$|*;12A z($k`&QU*)tV%6?d?qS%r-hN;C#oupbf$N>82r#J2uu6JSf8GpW^Jdn-XG~3DZ?TQH zJFtp9jBu-C7fp$ZPD_tSO0}81s2K@bph-k=VRClq0#)Z%Q@h=(-ouEkzum6!7QL@T z7T2BbS@^iGxr;0%iUNxctlutFyu2u)bF*ooi3;ki=&9sf{7qYZ+dZq={m4DJ)vwmM zZ+3EFap0M(lWLM+4(-(bGD;=X117XGVfZ(VIQkJ6o-4Qb+i}*UJ3pi6oO|A{9fTMR z3=(3XV305vLICKJcgoj0e{H(6vU73y#otC;cz$D+i-&*HNH?PVX!jjJLDVVKoPa$e z*8~de;GH_t(~`4y(0$Rt|5zKVcJFEEH;q{T-e&#Qn2rxBEd5=PEUYeXuDhal{+@|* zw$#C%JG@j8N@Tn2SD!b}g2&x+;rc6jYsi}`lZ7YvjlZ@2&1XOC)w|WP{TA(h8QIBc z>CT-7LxGJoBiqE`fdU$X*y9l)s&D7P{#X~QR=eF-Kg=73zQ9Jzmu};2_l~^x3u7;O z_Ir~qORPxdT;5bJmDQ=58gzXfx~UG`T#s(P@|tvg4XUOpRawQkxDuItn-!LP9cR(g zU2WrS_ie;->E8|WhGAFlcB|E~Pusa~6r#i<{usT-5qL(zi4JA&8evXP%Y}jv3X;-6 z(RZMP=12Pt_=Og+BC)5d|I$I;Fmze(YT!zDJABo<8@q@+7vgD6;jDiBN_ zy(T8OYi5MgTpS--m=@P5H#s>u8VRIQZl}1k!qE6)voj*IYjBK4?=fL3)F7ZzVK#k! zYH&`{PBRlCN}Nu!*&H5@_?*s?h=k0YCglXD=Id=?s2qku9RezavU+Sby(T|DH6|t) z6)`cX`S}{X&E`RQWjQQ`f~=^mUWdbBGMU0qVKSK<4u@B^SDu%;QnGUB&>>+#0$q=g GIT`>DaZ5G; literal 0 HcmV?d00001 diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml new file mode 100644 index 0000000..f8c6127 --- /dev/null +++ b/app/src/main/res/values/colors.xml @@ -0,0 +1,10 @@ + + + #FFBB86FC + #FF6200EE + #FF3700B3 + #FF03DAC5 + #FF018786 + #FF000000 + #FFFFFFFF + \ No newline at end of file diff --git a/app/src/main/res/values/ic_launcher_background.xml b/app/src/main/res/values/ic_launcher_background.xml new file mode 100644 index 0000000..ab57608 --- /dev/null +++ b/app/src/main/res/values/ic_launcher_background.xml @@ -0,0 +1,4 @@ + + + #02B342 + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml new file mode 100644 index 0000000..c6daf5e --- /dev/null +++ b/app/src/main/res/values/strings.xml @@ -0,0 +1,3 @@ + + Top Command + \ No newline at end of file diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml new file mode 100644 index 0000000..6c21ca8 --- /dev/null +++ b/app/src/main/res/values/themes.xml @@ -0,0 +1,5 @@ + + + +