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:
- Real Linux userspace: Based on proot + Ubuntu 24.04 ARM64, providing a complete Linux toolchain (Python, apt, bash, etc.)
- PTY pseudo-terminal: Standard
/dev/ptmxpseudo-terminal implementation, supporting interactive shell sessions - Foreground service keep-alive: Uses Android Foreground Service to decouple terminal sessions from the UI lifecycle, preventing them from being killed when the screen is off or the app is switched
- ACI cross-process invocation: 12 standardized capabilities, supporting calls from other apps via AIDL/HTTP/MCP
- Multiple IPC access: Four standard Android IPC methods — ContentProvider, Deep Link, Intent, BroadcastReceiver
- Multi-session management: Supports running multiple independent terminal sessions simultaneously
- Boot auto-start: Automatically restores terminal keep-alive after device reboot
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:
- Automatic environment detection: Checks
QuroLinuxEnv.isReady(), automatically selects proot or device shell - Timeout control: Default 14-second timeout to prevent command hanging
- Error handling: Catches all exceptions, returns readable error messages
- Process management: Manages child process lifecycle, supports interrupting execution
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:
- PTY pseudo-terminal: Standard
/dev/ptmximplementation, supports interactive shell - Process control:
fork/execcreates child process,TIOCSWINSZsets window size - Output stream reading: Asynchronously reads child process output, passes to UI via callback
- Session state: Tracks process PID, running status, start time
- Session destruction: Sends SIGTERM/SIGKILL signals, cleans up resources
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
- Format:
ubuntu-noble-aarch64-pd-v4.18.0.tar.xz - Size: ~80MB (compressed), ~300MB after extraction
- Download source: Ubuntu official mirrors (aliyun / tuna / cdimage)
- Path:
filesDir/linux-sandbox/rootfs, fully dynamic, no hardcoding
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:
- Low memory: System reclaims low-priority processes
- Battery optimization: Doze mode restricts background activity
- User manual cleanup: Swiping away from the recent tasks list
- App switching: After switching to another app, the original app may be killed
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:
- Terminal keep-alive does not belong to dataSync (no real data sync)
specialUserequires a<property>tag to explain usage, which Google Play reviews will check- 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
- Android Studio: 2024.1+ (Koala)
- JDK: 17+
- Android SDK: compileSdk 36, minSdk 26, targetSdk 34
- Gradle: 8.13+
- Kotlin: 2.3+
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:
- Test
execcapability: execute simple commands, complex commands, commands with timeout - Test
list_sessionscapability: view session status - Test
helpcapability: get help information
Cross-process testing:
- Test other apps calling terminal via ACI
- Test Intent/Provider/BroadcastReceiver access
Foreground service testing:
- Test if service continues running after screen off
- Test if service continues running after app switch
- Test if notification bar shows "Terminal running"
Linux environment testing:
- Test if proot environment works normally
- Test device shell fallback mechanism
- Test rootfs download and extraction
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.