initial commit

Signed-off-by: ale <ale@manalejandro.com>
Este commit está contenido en:
ale
2025-08-15 02:27:27 +02:00
commit 1cf7324c77
Se han modificado 55 ficheros con 4023 adiciones y 0 borrados

16
.gitignore vendido Archivo normal
Ver fichero

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

263
README.md Archivo normal
Ver fichero

@@ -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!** ⭐

1
app/.gitignore vendido Archivo normal
Ver fichero

@@ -0,0 +1 @@
/build

67
app/build.gradle.kts Archivo normal
Ver fichero

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

21
app/proguard-rules.pro vendido Archivo normal
Ver fichero

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

Ver fichero

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

Ver fichero

@@ -0,0 +1,27 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<application
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.TopCommand">
<activity
android:name=".MainActivity"
android:exported="true"
android:label="@string/app_name"
android:theme="@style/Theme.TopCommand">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>

Archivo binario no mostrado.

Después

Anchura:  |  Altura:  |  Tamaño: 16 KiB

Ver fichero

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

Ver fichero

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

Ver fichero

@@ -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<ProcessInfo> = withContext(Dispatchers.IO) {
val processes = mutableListOf<ProcessInfo>()
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>): 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<Long, Double> {
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<String> {
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<String> {
return try {
val mapsFile = File("/proc/$pid/maps")
if (!mapsFile.exists()) return emptyList()
mapsFile.readLines().take(8)
} catch (e: Exception) {
emptyList()
}
}
}

Ver fichero

@@ -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<ProcessRootInfo> = withContext(Dispatchers.IO) {
val processes = mutableListOf<ProcessRootInfo>()
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<ProcessRootInfo> {
val processes = mutableListOf<ProcessRootInfo>()
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<Double, Double, Double> {
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<String> {
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<String> {
return try {
val connections = mutableListOf<String>()
// 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<String> {
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<String> {
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<String, String> {
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<String, String> {
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<Double, Double, Double> = 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<String>,
val networkConnections: List<String>,
val memoryMaps: List<String>,
val environment: Map<String, String>,
val limits: Map<String, String>
)

Ver fichero

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

Ver fichero

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

Ver fichero

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

Ver fichero

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

Ver fichero

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

Ver fichero

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

Ver fichero

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

Ver fichero

@@ -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<List<ProcessInfo>>(emptyList())
val processes: StateFlow<List<ProcessInfo>> = _processes.asStateFlow()
private val _filteredProcesses = MutableStateFlow<List<ProcessInfo>>(emptyList())
val filteredProcesses: StateFlow<List<ProcessInfo>> = _filteredProcesses.asStateFlow()
private val _isLoading = MutableStateFlow(false)
val isLoading: StateFlow<Boolean> = _isLoading.asStateFlow()
private val _searchQuery = MutableStateFlow("")
val searchQuery: StateFlow<String> = _searchQuery.asStateFlow()
private val _sortBy = MutableStateFlow(SortBy.CPU)
val sortBy: StateFlow<SortBy> = _sortBy.asStateFlow()
private val _isAscending = MutableStateFlow(false)
val isAscending: StateFlow<Boolean> = _isAscending.asStateFlow()
private val _refreshInterval = MutableStateFlow(2000L)
val refreshInterval: StateFlow<Long> = _refreshInterval.asStateFlow()
private val _isAutoRefresh = MutableStateFlow(true)
val isAutoRefresh: StateFlow<Boolean> = _isAutoRefresh.asStateFlow()
// Root access states
private val _isRootAvailable = MutableStateFlow<Boolean?>(null)
val isRootAvailable: StateFlow<Boolean?> = _isRootAvailable.asStateFlow()
private val _isRootEnabled = MutableStateFlow(false)
val isRootEnabled: StateFlow<Boolean> = _isRootEnabled.asStateFlow()
private val _systemInfo = MutableStateFlow<SystemInfo?>(null)
val systemInfo: StateFlow<SystemInfo?> = _systemInfo.asStateFlow()
private val _rootError = MutableStateFlow<String?>(null)
val rootError: StateFlow<String?> = _rootError.asStateFlow()
// Process details states
private val _selectedProcessDetails = MutableStateFlow<ProcessDetailedInfo?>(null)
val selectedProcessDetails: StateFlow<ProcessDetailedInfo?> = _selectedProcessDetails.asStateFlow()
private val _isLoadingDetails = MutableStateFlow(false)
val isLoadingDetails: StateFlow<Boolean> = _isLoadingDetails.asStateFlow()
private val _showProcessDetails = MutableStateFlow(false)
val showProcessDetails: StateFlow<Boolean> = _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()
}
}
}

Ver fichero

@@ -0,0 +1,170 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path
android:fillColor="#3DDC84"
android:pathData="M0,0h108v108h-108z" />
<path
android:fillColor="#00000000"
android:pathData="M9,0L9,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,0L19,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,0L29,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,0L39,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,0L49,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,0L59,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,0L69,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,0L79,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M89,0L89,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M99,0L99,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,9L108,9"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,19L108,19"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,29L108,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,39L108,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,49L108,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,59L108,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,69L108,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,79L108,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,89L108,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,99L108,99"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,29L89,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,39L89,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,49L89,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,59L89,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,69L89,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,79L89,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,19L29,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,19L39,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,19L49,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,19L59,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,19L69,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,19L79,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
</vector>

Ver fichero

@@ -0,0 +1,30 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z">
<aapt:attr name="android:fillColor">
<gradient
android:endX="85.84757"
android:endY="92.4963"
android:startX="42.9492"
android:startY="49.59793"
android:type="linear">
<item
android:color="#44000000"
android:offset="0.0" />
<item
android:color="#00000000"
android:offset="1.0" />
</gradient>
</aapt:attr>
</path>
<path
android:fillColor="#FFFFFF"
android:fillType="nonZero"
android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z"
android:strokeWidth="1"
android:strokeColor="#00000000" />
</vector>

Ver fichero

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
</adaptive-icon>

Ver fichero

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
</adaptive-icon>

Archivo binario no mostrado.

Después

Anchura:  |  Altura:  |  Tamaño: 984 B

Archivo binario no mostrado.

Después

Anchura:  |  Altura:  |  Tamaño: 604 B

Archivo binario no mostrado.

Después

Anchura:  |  Altura:  |  Tamaño: 2.7 KiB

Archivo binario no mostrado.

Después

Anchura:  |  Altura:  |  Tamaño: 676 B

Archivo binario no mostrado.

Después

Anchura:  |  Altura:  |  Tamaño: 388 B

Archivo binario no mostrado.

Después

Anchura:  |  Altura:  |  Tamaño: 1.6 KiB

Archivo binario no mostrado.

Después

Anchura:  |  Altura:  |  Tamaño: 1.5 KiB

Archivo binario no mostrado.

Después

Anchura:  |  Altura:  |  Tamaño: 870 B

Archivo binario no mostrado.

Después

Anchura:  |  Altura:  |  Tamaño: 3.9 KiB

Archivo binario no mostrado.

Después

Anchura:  |  Altura:  |  Tamaño: 2.3 KiB

Archivo binario no mostrado.

Después

Anchura:  |  Altura:  |  Tamaño: 1.3 KiB

Archivo binario no mostrado.

Después

Anchura:  |  Altura:  |  Tamaño: 5.9 KiB

Archivo binario no mostrado.

Después

Anchura:  |  Altura:  |  Tamaño: 3.0 KiB

Archivo binario no mostrado.

Después

Anchura:  |  Altura:  |  Tamaño: 1.7 KiB

Archivo binario no mostrado.

Después

Anchura:  |  Altura:  |  Tamaño: 8.2 KiB

Ver fichero

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="purple_200">#FFBB86FC</color>
<color name="purple_500">#FF6200EE</color>
<color name="purple_700">#FF3700B3</color>
<color name="teal_200">#FF03DAC5</color>
<color name="teal_700">#FF018786</color>
<color name="black">#FF000000</color>
<color name="white">#FFFFFFFF</color>
</resources>

Ver fichero

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="ic_launcher_background">#02B342</color>
</resources>

Ver fichero

@@ -0,0 +1,3 @@
<resources>
<string name="app_name">Top Command</string>
</resources>

Ver fichero

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="Theme.TopCommand" parent="android:Theme.Material.Light.NoActionBar" />
</resources>

Ver fichero

@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?><!--
Sample backup rules file; uncomment and customize as necessary.
See https://developer.android.com/guide/topics/data/autobackup
for details.
Note: This file is ignored for devices older than API 31
See https://developer.android.com/about/versions/12/backup-restore
-->
<full-backup-content>
<!--
<include domain="sharedpref" path="."/>
<exclude domain="sharedpref" path="device.xml"/>
-->
</full-backup-content>

Ver fichero

@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?><!--
Sample data extraction rules file; uncomment and customize as necessary.
See https://developer.android.com/about/versions/12/backup-restore#xml-changes
for details.
-->
<data-extraction-rules>
<cloud-backup>
<!-- TODO: Use <include> and <exclude> to control what is backed up.
<include .../>
<exclude .../>
-->
</cloud-backup>
<!--
<device-transfer>
<include .../>
<exclude .../>
</device-transfer>
-->
</data-extraction-rules>

Ver fichero

@@ -0,0 +1,17 @@
package com.manalejandro.topcommand
import org.junit.Test
import org.junit.Assert.*
/**
* Example local unit test, which will execute on the development machine (host).
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
class ExampleUnitTest {
@Test
fun addition_isCorrect() {
assertEquals(4, 2 + 2)
}
}

6
build.gradle.kts Archivo normal
Ver fichero

@@ -0,0 +1,6 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
plugins {
alias(libs.plugins.android.application) apply false
alias(libs.plugins.kotlin.android) apply false
alias(libs.plugins.kotlin.compose) apply false
}

23
gradle.properties Archivo normal
Ver fichero

@@ -0,0 +1,23 @@
# Project-wide Gradle settings.
# IDE (e.g. Android Studio) users:
# Gradle settings configured through the IDE *will override*
# any settings specified in this file.
# For more details on how to configure your build environment visit
# http://www.gradle.org/docs/current/userguide/build_environment.html
# Specifies the JVM arguments used for the daemon process.
# The setting is particularly useful for tweaking memory settings.
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
# When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. For more details, visit
# https://developer.android.com/r/tools/gradle-multi-project-decoupled-projects
# org.gradle.parallel=true
# AndroidX package structure to make it clearer which packages are bundled with the
# Android operating system, and which are packaged with your app's APK
# https://developer.android.com/topic/libraries/support-library/androidx-rn
android.useAndroidX=true
# Kotlin code style for this project: "official" or "obsolete":
kotlin.code.style=official
# Enables namespacing of each library's R class so that its R class includes only the
# resources declared in the library itself and none from the library's dependencies,
# thereby reducing the size of the R class for that library
android.nonTransitiveRClass=true

31
gradle/libs.versions.toml Archivo normal
Ver fichero

@@ -0,0 +1,31 @@
[versions]
agp = "8.7.2"
kotlin = "2.0.21"
coreKtx = "1.13.1"
junit = "4.13.2"
junitVersion = "1.2.1"
espressoCore = "3.6.1"
lifecycleRuntimeKtx = "2.8.6"
activityCompose = "1.9.2"
composeBom = "2024.10.00"
[libraries]
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
junit = { group = "junit", name = "junit", version.ref = "junit" }
androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }
androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" }
androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycleRuntimeKtx" }
androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activityCompose" }
androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" }
androidx-ui = { group = "androidx.compose.ui", name = "ui" }
androidx-ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics" }
androidx-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" }
androidx-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" }
androidx-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" }
androidx-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" }
androidx-material3 = { group = "androidx.compose.material3", name = "material3" }
[plugins]
android-application = { id = "com.android.application", version.ref = "agp" }
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }

BIN
gradle/wrapper/gradle-wrapper.jar vendido Archivo normal

Archivo binario no mostrado.

6
gradle/wrapper/gradle-wrapper.properties vendido Archivo normal
Ver fichero

@@ -0,0 +1,6 @@
#Fri Aug 15 01:43:00 CEST 2025
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

185
gradlew vendido Archivo ejecutable
Ver fichero

@@ -0,0 +1,185 @@
#!/usr/bin/env sh
#
# Copyright 2015 the original author or authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
##############################################################################
##
## Gradle start up script for UN*X
##
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
PRG="$0"
# Need this for relative symlinks.
while [ -h "$PRG" ] ; do
ls=`ls -ld "$PRG"`
link=`expr "$ls" : '.*-> \(.*\)$'`
if expr "$link" : '/.*' > /dev/null; then
PRG="$link"
else
PRG=`dirname "$PRG"`"/$link"
fi
done
SAVED="`pwd`"
cd "`dirname \"$PRG\"`/" >/dev/null
APP_HOME="`pwd -P`"
cd "$SAVED" >/dev/null
APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"`
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum"
warn () {
echo "$*"
}
die () {
echo
echo "$*"
echo
exit 1
}
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "`uname`" in
CYGWIN* )
cygwin=true
;;
Darwin* )
darwin=true
;;
MINGW* )
msys=true
;;
NONSTOP* )
nonstop=true
;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD="$JAVA_HOME/jre/sh/java"
else
JAVACMD="$JAVA_HOME/bin/java"
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD="java"
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
# Increase the maximum file descriptors if we can.
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
MAX_FD_LIMIT=`ulimit -H -n`
if [ $? -eq 0 ] ; then
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
MAX_FD="$MAX_FD_LIMIT"
fi
ulimit -n $MAX_FD
if [ $? -ne 0 ] ; then
warn "Could not set maximum file descriptor limit: $MAX_FD"
fi
else
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
fi
fi
# For Darwin, add options to specify how the application appears in the dock
if $darwin; then
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
fi
# For Cygwin or MSYS, switch paths to Windows format before running java
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
JAVACMD=`cygpath --unix "$JAVACMD"`
# We build the pattern for arguments to be converted via cygpath
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
SEP=""
for dir in $ROOTDIRSRAW ; do
ROOTDIRS="$ROOTDIRS$SEP$dir"
SEP="|"
done
OURCYGPATTERN="(^($ROOTDIRS))"
# Add a user-defined pattern to the cygpath arguments
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
fi
# Now convert the arguments - kludge to limit ourselves to /bin/sh
i=0
for arg in "$@" ; do
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
else
eval `echo args$i`="\"$arg\""
fi
i=`expr $i + 1`
done
case $i in
0) set -- ;;
1) set -- "$args0" ;;
2) set -- "$args0" "$args1" ;;
3) set -- "$args0" "$args1" "$args2" ;;
4) set -- "$args0" "$args1" "$args2" "$args3" ;;
5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
esac
fi
# Escape application args
save () {
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
echo " "
}
APP_ARGS=`save "$@"`
# Collect all arguments for the java command, following the shell quoting and substitution rules
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
exec "$JAVACMD" "$@"

89
gradlew.bat vendido Archivo normal
Ver fichero

@@ -0,0 +1,89 @@
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@if "%DEBUG%" == "" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto execute
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
:end
@rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="0" goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega

BIN
screenshot.png Archivo normal

Archivo binario no mostrado.

Después

Anchura:  |  Altura:  |  Tamaño: 100 KiB

23
settings.gradle.kts Archivo normal
Ver fichero

@@ -0,0 +1,23 @@
pluginManagement {
repositories {
google {
content {
includeGroupByRegex("com\\.android.*")
includeGroupByRegex("com\\.google.*")
includeGroupByRegex("androidx.*")
}
}
mavenCentral()
gradlePluginPortal()
}
}
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
google()
mavenCentral()
}
}
rootProject.name = "Top Command"
include(":app")