跪拜 Guibai
← Back to the summary

An Android Terminal That Survives Screen-Off: Inside Zorv AI's proot Sandbox

Zorv AI Terminal: Full Architecture Analysis of an Android Terminal Emulator Based on proot Sandbox

Author: QUOR Tags: GitHub

1. Introduction

Zorv AI Terminal is a complete Android terminal emulator integrated within the Zorv AI application. It is not merely a simple command-line interface, but a full terminal system with the following core capabilities:

Open-source repository: https://github.com/Quor-a/ZorvAI


2. Architecture Design

2.1 Overall Architecture Diagram

┌─────────────────────────────────────────────────────────────────────┐
│                          Terminal UI Layer                           │
│  ChatScreen input box "+" → Terminal / AI call ui_open_terminal     │
└───────────────────────────┬─────────────────────────────────────────┘
                            │
┌───────────────────────────▼─────────────────────────────────────────┐
│                      QuroTerminalController                         │
│  Session management · Command routing · proot/device shell auto-    │
│  selection · Timeout control                                        │
└───┬──────────────────────┬──────────────────────┬───────────────────┘
    │                      │                      │
┌───▼───────────┐  ┌───────▼────────┐  ┌─────────▼──────────────────┐
│QuroShellSession│  │QuroLinuxEnv    │  │QuroTerminalSessionManager  │
│PTY session     │  │proot+Ubuntu    │  │Multi-session mgmt · Cross- │
│carrier         │  │24.04 ARM64     │  │process access              │
│/dev/ptmx      │  │rootfs download/│  │Default/extra/UI/history     │
│fork/exec      │  │extraction      │  │sessions                    │
└───┬───────────┘  └───────┬────────┘  └─────────┬──────────────────┘
    │                      │                      │
┌───▼──────────────────────▼──────────────────────▼───────────────────┐
│                     Foreground Service Layer (Keep-Alive)            │
│   QuroTerminalKeepAliveService · QuroTerminalAciService             │
│   specialUse foreground service · shell child process belongs to    │
│   service process · 15-second patrol                                │
└───┬──────────────────────┬──────────────────────┬───────────────────┘
    │                      │                      │
┌───▼───────────┐  ┌───────▼────────┐  ┌─────────▼──────────────────┐
│ACI Cross-     │  │Intent/Provider │  │BroadcastReceiver/DeepLink  │
│process        │  │ContentProvider │  │6 broadcast Actions         │
│12 capabilities│  │content:// URI  │  │quro://terminal/...         │
│AIDL binding   │  │                │  │                            │
└───────────────┘  └────────────────┘  └────────────────────────────┘

2.2 Design Principles

Principle Description
Process ownership The shell child process must belong to the foreground service process; service alive = terminal alive
Automatic environment selection Prefer the proot Linux environment; automatically degrade to device shell when unavailable
Multi-session isolation Each terminal session runs independently without interference
Cross-process standardization Expose capabilities through the ACI protocol and standard Android IPC
Android version compatibility Supports Android 8.0 (API 26) through Android 15+

2.3 Technology Choices

Technology Choice Reason
Linux userspace proot + Ubuntu 24.04 ARM64 No ROOT required, full Ubuntu toolchain
Pseudo-terminal /dev/ptmx PTY Standard Linux pseudo-terminal, supports interactive shell
Foreground service specialUse type Android 14+ compatible, no real data sync activity needed
Cross-process communication ACI AIDL + HTTP + MCP Multiple invocation methods for different scenarios
IPC access Provider + DeepLink + Intent + Broadcast Standard Android IPC, no special permissions required
Session management SessionManager singleton Unified management of all sessions, supports cross-process access

3. Core Component Details

3.1 QuroTerminalController — Terminal Controller

File location: app/src/main/java/com/ai/assistance/quro/core/terminal/QuroTerminalController.kt

Responsibility: Core controller for the terminal, responsible for command routing, environment selection, and timeout control.

Core capabilities:

// Command execution entry point
fun runCommand(command: String, timeout: Long = 14000): String {
    val env = QuroLinuxEnv.getInstance(context)
    return if (env.isReady()) {
        // Linux environment available → use proot
        runCommandInLinux(command, timeout)
    } else {
        // Fall back to device shell
        runCommandInDeviceShell(command, timeout)
    }
}

Key features:

3.2 QuroShellSession — PTY Shell Session Carrier

File location: app/src/main/java/com/ai/assistance/quro/core/terminal/QuroShellSession.kt

Responsibility: Complete implementation of PTY pseudo-terminal, managing shell process creation, communication, and destruction.

Core capabilities:

// Session creation
companion object {
    suspend fun create(
        context: Context,
        env: Map<String, String>,
        name: String = "default",
        onOutput: (String) -> Unit
    ): QuroShellSession {
        // 1. Open pseudo-terminal master device
        val masterFd = Os.open("/dev/ptmx", O_RDWR or O_NOCTTY)
        // 2. Grant and unlock slave device
        Os.grantpt(masterFd)
        Os.unlockpt(masterFd)
        // 3. Get slave device name
        val slaveName = Os.slavename(masterFd)
        // 4. Open slave device
        val slaveFd = Os.open(slaveName, O_RDWR or O_NOCTTY)
        // 5. Set window size
        val winsize = Winsize(24, 80, 0, 0)
        Os.ioctl(masterFd, TIOCSWINSZ, winsize)
        // 6. Fork child process
        val pid = fork()
        if (pid == 0) {
            // Child process: redirect standard I/O to slave device
            Os.dup2(slaveFd, 0)
            Os.dup2(slaveFd, 1)
            Os.dup2(slaveFd, 2)
            Os.execve("/bin/sh", arrayOf("/bin/sh"), envp)
        }
        // 7. Parent process: create session object
        return QuroShellSession(masterFd, pid, name, onOutput)
    }
}

Key features:

3.3 QuroTerminalSessionManager — Multi-Session Manager

File location: app/src/main/java/com/ai/assistance/quro/core/terminal/QuroTerminalSessionManager.kt

Responsibility: Unified management of all terminal sessions, supporting multi-session concurrency and cross-process access.

Core capabilities:

// Session manager (singleton)
object QuroTerminalSessionManager {
    // Session storage
    private val sessions = mutableMapOf<String, QuroShellSession>()

    // Create session
    suspend fun createSession(
        context: Context,
        name: String = "session_${System.currentTimeMillis()}",
        installIfMissing: Boolean = false
    ): QuroShellSession {
        val env = QuroLinuxEnv.getInstance(context)
        if (installIfMissing && !env.isReady()) {
            env.ensureInstalled(context)
        }
        val session = QuroShellSession.create(context, env.getEnv(), name) { output ->
            // Output callback
        }
        sessions[session.id] = session
        return session
    }

    // Get session
    fun getSession(sessionId: String): QuroShellSession? = sessions[sessionId]

    // List all sessions
    fun listSessions(): List<Map<String, Any>> {
        return sessions.map { (id, session) ->
            mapOf(
                "id" to id,
                "name" to session.name,
                "is_alive" to session.isAlive(),
                "pid" to session.pid,
                "uptime" to session.getUptime()
            )
        }
    }

    // Destroy session
    fun destroySession(sessionId: String): Boolean {
        val session = sessions.remove(sessionId) ?: return false
        session.destroy()
        return true
    }
}

Session types:

Type Description Lifecycle
Default session Session used by the main terminal interface Foreground service keep-alive, longest lifecycle
Extra session Session manually created by the user Follows the app process
UI session Session for the terminal UI interface Follows the UI lifecycle
History session Record of a terminated session Only retains status information

3.4 QuroLinuxEnv — Linux Environment Backend

File location: app/src/main/java/com/ai/assistance/quro/core/linux/QuroLinuxEnv.kt

Responsibility: Manages the proot + Ubuntu 24.04 ARM64 userspace, providing installation, detection, and configuration of the Linux environment.

Core capabilities:

class QuroLinuxEnv(private val context: Context) {
    // Path configuration (fully dynamic, no hardcoding)
    private val rootfsPath = File(context.filesDir, "linux-sandbox/rootfs")
    private val prootPath = "${context.applicationInfo.nativeLibraryDir}/libproot.so"
    private val homePath = context.getExternalFilesDir(null)

    // Check if Linux environment is ready
    fun isReady(): Boolean {
        return rootfsPath.exists() &&
               File(rootfsPath, "usr/bin/sh").exists() &&
               File(prootPath).exists()
    }

    // Install Linux environment (download rootfs)
    suspend fun ensureInstalled(context: Context) {
        if (isReady()) return
        // Download rootfs from Ubuntu official mirror
        downloadRootfs(context)
        // Extract rootfs
        extractRootfs(context)
        // Configure apt sources
        configureAptSources()
    }

    // Get proot launch arguments
    fun getProotArgs(): List<String> {
        return listOf(
            "-0", "root",
            "--link2symlink",
            "-w", "/root",
            "--bind=/proc",
            "--bind=/sys",
            "--bind=/dev",
            "--bind=/sdcard:/mnt/sdcard"
        )
    }

    // Get environment variables
    fun getEnv(): Map<String, String> {
        return mapOf(
            "HOME" to "/root",
            "PATH" to "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
            "TERM" to "xterm-256color",
            "LANG" to "en_US.UTF-8",
            "TMPDIR" to "/tmp"
        )
    }
}

Technical details:

Item Description
rootfs format ubuntu-noble-aarch64-pd-v4.18.0.tar.xz (Ubuntu 24.04 Noble ARM64)
rootfs size ~80MB (compressed), ~300MB after extraction
Download source Ubuntu official mirrors (aliyun / tuna / cdimage)
Built-in tools proot (as .so), libbash, libbusybox
Path rootfsPath=File(context.filesDir,"linux-sandbox"), fully dynamic, no hardcoding

3.5 QuroTerminalKeepAliveService — Foreground Keep-Alive Service

File location: app/src/main/java/com/ai/assistance/quro/service/QuroTerminalKeepAliveService.kt

Responsibility: Lives as a foreground service, decoupling terminal sessions from the UI lifecycle so they are not killed when the screen is off or the app is switched.

Core principle:

Foreground service calls startForeground()
    → System does not kill this process
    → Shell child process forked within the process is also not killed
    → Survives screen-off / app switch

Core code:

class QuroTerminalKeepAliveService : Service() {
    private var heldSession: QuroShellSession? = null  // Service directly holds terminal session

    override fun onCreate() {
        // Start foreground service (specialUse type)
        startForeground(NOTIF_ID, buildNotification("Terminal running…"),
            ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE)

        // Start patrol loop
        startLoop()
    }

    private fun ensureSessionSafe() {
        // Create shell child process within the service process
        heldSession = QuroShellSession.create(
            context = this,
            env = QuroLinuxEnv.getInstance(this).getEnv(),
            name = "keepalive"
        ) { output ->
            // Output callback
        }
        // Shell child process is a fork of the service process → service alive = shell child process alive
    }

    private fun startLoop() {
        // Patrol every 15 seconds
        coroutineScope.launch {
            while (isActive) {
                ensureSessionSafe()  // Ensure session is alive
                ensureAciService()  // Ensure ACI service is running
                updateNotification() // Update notification
                delay(15_000)       // 15-second interval
            }
        }
    }
}

Key features:

Feature Implementation
Foreground service type specialUse (Android 14+ compatible)
Patrol interval Every 15 seconds checks session status, auto-rebuilds if dead
Notification bar Persistent "Zorv AI Terminal running", tap to jump to main interface
Session ownership Shell child process belongs to service process, service alive = terminal alive
ACI service management Automatically starts/restarts QuroTerminalAciService

3.6 QuroTerminalAciService — ACI Controlled Endpoint Service

File location: app/src/main/java/com/ai/assistance/quro/service/QuroTerminalAciService.kt

Responsibility: Extends BaseAidlAciService, exposing all terminal capabilities to external applications.

12 ACI capabilities:

Capability Input Return Description
exec command(required) / timeout(optional) / session_id(optional) output / exit_code / error Execute command
create_session name(optional) session_id / name Create session
destroy_session session_id(required) destroyed Destroy session
send_input session_id(required) / input(required) sent Send input
get_session_status session_id(required) session_id / is_alive / pid / uptime Session status
list_sessions sessions (array) List all sessions
set_session_env session_id / key / value set Set environment variable
get_session_env session_id / key value Get environment variable
list_capabilities capabilities (array) List capabilities
get_service_status running / session_count / uptime Service status
get_audit_log limit(optional) logs (array) Audit log
help help_text Help information

4. Linux Sandbox (proot + Ubuntu 24.04 ARM64)

4.1 Architecture Principle

┌─────────────────────────────────────────┐
│           Android App Process            │
│                                         │
│  ┌─────────────────────────────────┐    │
│  │         proot Process            │    │
│  │  ┌─────────────────────────┐    │    │
│  │  │    Ubuntu 24.04 ARM64   │    │    │
│  │  │    rootfs Userspace     │    │    │
│  │  │                         │    │    │
│  │  │  /bin/sh  ← terminal    │    │    │
│  │  │            shell        │    │    │
│  │  │  /usr/bin/python3       │    │    │
│  │  │  /usr/bin/apt           │    │    │
│  │  │  /usr/bin/bash          │    │    │
│  │  └─────────────────────────┘    │    │
│  │                                 │    │
│  │  --bind=/proc  ← mount proc    │    │
│  │  --bind=/sys   ← mount sys     │    │
│  │  --bind=/dev   ← mount dev     │    │
│  │  --link2symlink ← symlink      │    │
│  │                  compat         │    │
│  └─────────────────────────────────┘    │
│                                         │
│  proot path: nativeLibraryDir/libproot.so│
│  rootfs: filesDir/linux-sandbox/rootfs  │
└─────────────────────────────────────────┘

4.2 rootfs Management

4.3 Built-in Toolchain

Tool Description
proot Built-in as .so, loaded via nativeLibraryDir
libbash Bash shell support
libbusybox Lightweight Unix toolset
python3 Python 3 runtime
apt Package manager, can install more tools

4.4 CMS Runtime Integration

# Install runtimes via bootstrap script
bootstrap.sh --install NODE    # Node.js
bootstrap.sh --install PYTHON  # Python
bootstrap.sh --install RUST    # Rust
bootstrap.sh --install GO      # Go
bootstrap.sh --install JAVA    # Java
bootstrap.sh --install SSH     # OpenSSH

Runtime sharing: All terminal sessions share the same set of CMS runtimes, no need for repeated installation.


5. Foreground Service Keep-Alive Mechanism

5.1 Why Foreground Service is Needed

The Android system kills background processes under the following conditions:

Foreground service is the highest-priority service type provided by Android; the system almost never kills a foreground service process. Therefore, making terminal sessions belong to the foreground service process ensures they survive screen-off / app switch.

5.2 specialUse Type Selection

Android 14+ has strict restrictions on foreground service types:

Type Restriction Applicable Scenario
dataSync Must have real data sync activity File sync, cloud backup
mediaPlayback Must be playing media Music player
location Must be acquiring location Navigation app
specialUse Requires <property> tag to explain usage Terminal keep-alive (our choice)

Reasons for choosing specialUse:

  1. Terminal keep-alive does not belong to dataSync (no real data sync)
  2. specialUse requires a <property> tag to explain usage, which Google Play reviews will check
  3. After adding <property>, startForeground() will not be silently rejected by the system

5.3 Session Ownership Model

Traditional model (no keep-alive):
App process → fork shell child process → App killed → child process also killed

Foreground service model (keep-alive):
App process → Foreground service → fork shell child process
    ↑
    System does not kill this process
    → Shell child process also not killed
    → Survives screen-off / app switch

Key code:

// QuroTerminalKeepAliveService.kt
private var heldSession: QuroShellSession? = null

private fun ensureSessionSafe() {
    // Create shell child process within the service process
    heldSession = QuroShellSession.create(
        context = this,
        env = QuroLinuxEnv.getInstance(this).getEnv(),
        name = "keepalive"
    ) { output ->
        // Output callback
    }
    // Shell child process is a fork of the service process
    // Service alive = shell child process alive
}

5.4 Patrol and Self-Healing Mechanism

private fun startLoop() {
    coroutineScope.launch {
        while (isActive) {
            // 1. Check if session is alive
            val session = heldSession
            if (session == null || !session.isAlive()) {
                Log.w(TAG, "Session died, rebuilding…")
                ensureSessionSafe()
            }

            // 2. Check if ACI service is running
            ensureAciService()

            // 3. Update notification
            updateNotification()

            // 4. Wait 15 seconds
            delay(15_000)
        }
    }
}

5.5 Android 14+ Compatibility

Manifest configuration:

<!-- Permission declarations -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE" />

<!-- Service declaration -->
<service
    android:name=".service.QuroTerminalKeepAliveService"
    android:exported="false"
    android:foregroundServiceType="specialUse">
    <property
        android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE"
        android:value="Terminal session keep-alive: Keeps terminal sessions from being killed when screen is off or app is switched" />
</service>

Startup code:

override fun onCreate() {
    super.onCreate()
    try {
        startForeground(
            NOTIF_ID,
            buildNotification("Terminal running…"),
            ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE
        )
    } catch (e: Throwable) {
        Log.e(TAG, "Foreground notification creation failed", e)
        stopSelf()
        return
    }
}

5.6 Boot Auto-Start

// QuroTerminalBootReceiver.kt
class QuroTerminalBootReceiver : BroadcastReceiver() {
    override fun onReceive(context: Context, intent: Intent) {
        if (intent.action == Intent.ACTION_BOOT_COMPLETED) {
            // Auto-start foreground service on boot
            val serviceIntent = Intent(context, QuroTerminalKeepAliveService::class.java)
            context.startForegroundService(serviceIntent)
        }
    }
}

Manifest registration:

<receiver
    android:name=".service.QuroTerminalBootReceiver"
    android:exported="true">
    <intent-filter>
        <action android:name="android.intent.action.BOOT_COMPLETED" />
    </intent-filter>
</receiver>

6. PTY Pseudo-Terminal Implementation

6.1 Pseudo-Terminal Working Principle

┌─────────────────────────────────────────────────┐
│                  Terminal UI Process             │
│                                                 │
│  ┌─────────────────────────────────────────┐    │
│  │         master fd (master device)       │    │
│  │  /dev/ptmx                              │    │
│  └──────────────────┬──────────────────────┘    │
│                     │                           │
│                     │ Kernel pseudo-terminal    │
│                     │ driver                    │
│                     │                           │
│  ┌──────────────────▼──────────────────────┐    │
│  │         slave fd (slave device)         │    │
│  │  /dev/pts/N                             │    │
│  └──────────────────┬──────────────────────┘    │
│                     │                           │
│                     │ dup2 redirection          │
│                     │                           │
│  ┌──────────────────▼──────────────────────┐    │
│  │         shell child process             │    │
│  │  /bin/sh                                │    │
│  │  stdin  ← slave fd                      │    │
│  │  stdout → slave fd                      │    │
│  │  stderr → slave fd                      │    │
│  └─────────────────────────────────────────┘    │
└─────────────────────────────────────────────────┘

6.2 Core Code Implementation

// QuroShellSession.kt core creation logic
val masterFd = Os.open("/dev/ptmx", O_RDWR or O_NOCTTY)
val slaveName = Os.slavename(masterFd)
Os.grantpt(masterFd)
Os.unlockpt(masterFd)
val slaveFd = Os.open(slaveName, O_RDWR or O_NOCTTY)

// Set window size
val winsize = Winsize(24, 80, 0, 0)
Os.ioctl(masterFd, TIOCSWINSZ, winsize)

// Create new session (required)
Os.setsid()

// Fork child process
val pid = Os.fork()
if (pid == 0) {
    // Child process
    Os.dup2(slaveFd, 0)  // stdin
    Os.dup2(slaveFd, 1)  // stdout
    Os.dup2(slaveFd, 2)  // stderr
    Os.execve("/bin/sh", arrayOf("/bin/sh"), envp)
}

6.3 Output Stream Handling

// Asynchronous output reading
private fun readOutputLoop() {
    val buffer = ByteArray(4096)
    while (isRunning) {
        val bytesRead = Os.read(masterFd, buffer)
        if (bytesRead > 0) {
            val output = String(buffer, 0, bytesRead)
            onOutput(output)  // Callback to UI
        } else if (bytesRead == 0) {
            break  // EOF
        }
    }
}

7. Cross-Process Access Methods

7.1 ContentProvider (TerminalProvider)

Authority: content://com.ai.assistance.quro.terminal

Supported paths:

Path Method Description
/sessions query List all sessions
/exec?cmd=... query Execute command and return result
/status query Get service status
/session/{id} query Get specified session info

Usage example:

// List sessions
val cursor = contentResolver.query(
    Uri.parse("content://com.ai.assistance.quro.terminal/sessions"),
    null, null, null, null
)

// Execute command
val cursor = contentResolver.query(
    Uri.parse("content://com.ai.assistance.quro.terminal/exec?cmd=uname -a"),
    null, null, null, null
)

7.2 Deep Link (TerminalDeepLinkHandler)

Scheme: quro://terminal/...

Supported paths:

Path Description
exec?cmd=... Execute command
sessions Session list
create?name=... Create session
status Service status

Usage example:

// Execute command
val intent = Intent(Intent.ACTION_VIEW,
    Uri.parse("quro://terminal/exec?cmd=python3 --version"))
startActivity(intent)

// Create session
val intent = Intent(Intent.ACTION_VIEW,
    Uri.parse("quro://terminal/create?name=my-session"))
startActivity(intent)

7.3 Intent Handler (TerminalIntentHandler)

Supported Actions:

Action Extra Description
com.ai.assistance.quro.action.TERMINAL_EXEC command Execute command
com.ai.assistance.quro.action.TERMINAL_STATUS Get status
com.ai.assistance.quro.action.TERMINAL_SESSIONS List sessions
com.ai.assistance.quro.action.TERMINAL_CREATE_SESSION name Create session

Usage example:

// Execute command
val intent = Intent("com.ai.assistance.quro.action.TERMINAL_EXEC")
intent.putExtra("command", "ls -la /home")
sendBroadcast(intent)

7.4 BroadcastReceiver (TerminalBroadcastReceiver)

Supported Actions (6):

Action Extra Return
TERMINAL_EXEC command output / exit_code / error
TERMINAL_STATUS running / session_count
TERMINAL_SESSIONS sessions (array)
TERMINAL_CREATE_SESSION name session_id
TERMINAL_DESTROY_SESSION session_id destroyed
TERMINAL_SEND_INPUT session_id / input sent

Usage example:

// Send broadcast and receive result
val receiver = object : BroadcastReceiver() {
    override fun onReceive(context: Context, intent: Intent) {
        val output = intent.getStringExtra("output")
        val exitCode = intent.getIntExtra("exit_code", -1)
        Log.d("Terminal", "Output: $output, Exit: $exitCode")
    }
}

registerReceiver(receiver, IntentFilter("com.ai.assistance.quro.action.TERMINAL_RESULT"))

val intent = Intent("com.ai.assistance.quro.action.TERMINAL_EXEC")
intent.putExtra("command", "echo hello")
sendBroadcast(intent)

7.5 Comparison and Selection

Method Applicable Scenario Advantages Disadvantages
ContentProvider Data query, cross-app data sharing Standard Android API, supports CRUD Not suitable for long-running commands
Deep Link User clicks link to trigger action Intuitive, supports URL sharing Not suitable for background calls
Intent Simple inter-app communication Simple, supports extras Result return requires extra mechanism
BroadcastReceiver Async notification, event-driven Decoupled, supports one-to-many Result return requires extra registration
ACI Complex cross-process calls 12 standardized capabilities, supports AIDL/HTTP/MCP Requires service binding

8. Command Execution Routing

8.1 Automatic Environment Detection

// QuroTerminalController.kt
fun runCommand(command: String, timeout: Long = 14000): String {
    val env = QuroLinuxEnv.getInstance(context)
    return if (env.isReady()) {
        // Linux environment available → use proot
        runCommandInLinux(command, timeout)
    } else {
        // Fall back to device shell
        runCommandInDeviceShell(command, timeout)
    }
}

8.2 proot Command Construction

private fun runCommandInLinux(command: String, timeout: Long): String {
    val prootPath = "${applicationInfo.nativeLibraryDir}/libproot.so"
    val prootArgs = QuroLinuxEnv.getInstance(context).getProotArgs()
    // Directly use prootArgs + command, do not duplicate parameters
    val fullCommand = listOf(prootPath) + prootArgs + listOf("/bin/sh", "-c", command)

    val process = ProcessBuilder(fullCommand)
        .redirectErrorStream(true)
        .start()

    // Timeout control
    val completed = process.waitFor(timeout, TimeUnit.MILLISECONDS)
    if (!completed) {
        process.destroyForcibly()
        throw TimeoutException("Command execution timed out: $command")
    }

    return process.inputStream.bufferedReader().readText()
}

8.3 Timeout and Error Handling

// Timeout control
val completed = process.waitFor(timeout, TimeUnit.MILLISECONDS)
if (!completed) {
    process.destroyForcibly()
    throw TimeoutException("Command execution timed out: $command")
}

// Error handling
try {
    val result = runCommand(command, timeout)
    return mapOf(
        "output" to result,
        "exit_code" to 0,
        "error" to ""
    )
} catch (e: TimeoutException) {
    return mapOf(
        "output" to "",
        "exit_code" to -1,
        "error" to "Command execution timed out"
    )
} catch (e: Exception) {
    return mapOf(
        "output" to "",
        "exit_code" to -1,
        "error" to e.message ?: "Unknown error"
    )
}

9. Terminal UI Integration

9.1 Entry Points

Entry Description
Dialog input box "+" Click the "+" button on the left side of the input box, select "Terminal"
AI call ui_open_terminal AI actively opens the terminal interface during conversation
Deep Link quro://terminal/exec?cmd=... directly launches
ACI cross-process call Other apps call terminal capabilities via ACI protocol

9.2 Session Switching

// QuroTerminalSessionManager.kt
// Create new session
val newSession = QuroTerminalSessionManager.createSession(context, "my-session")

// Switch to specified session
val session = QuroTerminalSessionManager.getSession("session-id")

// Destroy session
QuroTerminalSessionManager.destroySession("session-id")

9.3 Input/Output Rendering

Terminal UI is implemented using Jetpack Compose:

@Composable
fun TerminalScreen() {
    val output = remember { mutableStateListOf<String>() }

    LaunchedEffect(Unit) {
        // Start terminal session
        val session = QuroTerminalSessionManager.createSession(context) { line ->
            output.add(line)
        }
    }

    LazyColumn {
        items(output) { line ->
            Text(
                text = line,
                fontFamily = FontFamily.Monospace,
                fontSize = 12.sp
            )
        }
    }
}

10. Key Feature Summary

Feature Status Description
Real userspace Ubuntu 24.04 ARM64, complete Linux toolchain
PTY pseudo-terminal /dev/ptmx + fork/exec + TIOCSWINSZ
Foreground service keep-alive specialUse type, survives screen-off / app switch
ACI cross-process 12 capabilities, AIDL/HTTP/MCP three invocation methods
Intent/Provider ContentProvider + Deep Link + Intent + BroadcastReceiver
Multi-session support Default/extra/UI/history sessions, session isolation
Boot auto-start BOOT_COMPLETED broadcast receiver
Android 14+ compatibility specialUse + <property> tag
proot sandbox No ROOT required, link2symlink symlink handling
CMS runtimes NODE/PYTHON/RUST/GO/JAVA shared environment
Timeout control Default 14-second timeout, prevents command hanging
Error handling Comprehensive exception catching and error returns

11. Common Issues and Troubleshooting

Symptom Explanation / Handling
Terminal killed after screen off / app switch Confirm foreground service is started: notification bar should show "Zorv AI Terminal running". Check AndroidManifest for foregroundServiceType="specialUse" and <property> tag
Terminal ACI cross-process call fails Check if QuroTerminalAciService is registered in Manifest, and if permission ai.aci.permission.CALL is declared
Terminal Intent/Provider not responding Check if TerminalProvider, TerminalBroadcastReceiver, TerminalDeepLinkHandler are registered in Manifest
Terminal session state inconsistent QuroTerminalSessionManager manages multiple sessions; call listSessions() to get real status
Terminal command execution reports Illegal option -0 proot parameter duplication issue, fixed in update to v1.0.67+
In-app Linux (L5) cannot run First time entering terminal will prompt "Install Linux environment"; rootfs needs network to download from Ubuntu official mirror
Command execution timeout Default 14-second timeout, adjustable via timeout parameter
Session output garbled Check if TERM environment variable is set to xterm-256color

12. Development Guide

12.1 Environment Setup

12.2 Building from Source

# Clone repository
git clone https://github.com/Quor-a/ZorvAI.git
cd ZorvAI

# Build full release APK
./gradlew :app:assembleFullRelease

# Output path
# app/build/outputs/apk/full/release/app-full-release.apk

12.3 Adding New Terminal Capabilities

Step 1: Add capability definition in QuroTerminalAciService.kt's onCreateCapabilities

override fun onCreateCapabilities(): List<AidlAciCapability> {
    val caps = super.onCreateCapabilities().toMutableList()
    caps.add(AidlAciCapability("my_new_capability", "My new capability"))
    return caps
}

Step 2: Add capability handling logic in onCall

override fun onCall(request: AidlAciRequest): AidlAciResponse {
    return when (request.capability) {
        "my_new_capability" -> handleMyNewCapability(request.params)
        // ... other capabilities
    }
}

12.4 Testing Suggestions

Functional testing:

Cross-process testing:

Foreground service testing:

Linux environment testing:


13. Open-Source Information

Item Information
Open-source repository https://github.com/Quor-a/ZorvAI
ACI Developer Guide docs/ACI_DEVELOPER_GUIDE.md
Current version v1.0.67
License Apache License 2.0
Tech stack Kotlin 2.3 + Jetpack Compose
Minimum support Android 8.0 (API 26)
Target version Android 14 (API 34)

This document is maintained by the Zorv AI development team. For questions or suggestions, please provide feedback on GitHub Issues.