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

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