Skip to content

Commit aaa8d0b

Browse files
authored
v1.8.0: AI Providers, SSH Access, Ctrl keys, Configure menu, resolv.conf fix (#36-#40) (#42)
* Fix Ctrl key for arrows/special keys, add Configure menu, clickable URL, SSH package (#37, #38) - Fix Ctrl+Arrow/Home/End/PgUp/PgDn sending raw sequences instead of xterm Ctrl-modified variants (e.g. \x1b[1;5A for Ctrl+Up) - Make gateway dashboard URL clickable (opens WebDashboardScreen) with primary color + underline styling and open_in_new icon button - Add "Configure" quick action card running `openclaw configure` in terminal - Add OpenSSH as optional package (apt-get install openssh-client openssh-server) Co-Authored-By: Mithun Gowda B <mithungowda.b7411@gmail.com> * Add multi-provider AI model selection and SSH remote access - Add AiProvider data model with 7 built-in providers (Anthropic, OpenAI, Google Gemini, OpenRouter, NVIDIA NIM, DeepSeek, xAI) - Add ProviderConfigService to read/write provider config in openclaw.json using Node.js safe-merge pattern - Add ProvidersScreen listing all providers with Active/Configured badges - Add ProviderDetailScreen with API key form and model dropdown - Add SshService for sshd lifecycle (start/stop, password, IP discovery) - Add SshScreen with service control, password management, and connection info with copyable ssh commands - Add AI Providers and SSH Access cards to dashboard Co-Authored-By: Mithun Gowda B <mithungowda.b7411@gmail.com> * Fix resolv.conf ENOENT after app upgrade (#40) After upgrading to v1.7.3, Android may clear the app's files directory. writeResolvConf() only created the config/ directory but other required directories (tmp/, home/, lib/, proc/sys fakes) were missing, causing proot bind-mount failures. Call setupDirectories() instead of a bare mkdirs() so the entire directory tree is recreated before writing resolv.conf. Also use the two-argument File constructor for correctness. Fixes #40 Co-Authored-By: Mithun Gowda B <mithungowda.b7411@gmail.com> * Call setupDirs before writeResolv in gateway init and start (#40) On a normal app launch (setup already complete), setupDirs was never called. If Android cleared the files directory after an upgrade, the config/ directory was missing when writeResolv ran. Now both init() and start() call setupDirs() first, matching the bootstrap flow. The Kotlin-side writeResolvConf() also calls setupDirectories() internally as a safety net. Co-Authored-By: Mithun Gowda B <mithungowda.b7411@gmail.com> * Fix writeResolvConf: only mkdirs config dir, not full setupDirectories (#40) setupDirectories() also runs setupLibtalloc() and setupFakeSysdata() which can fail during first-time setup when rootfs hasn't been extracted yet. writeResolvConf() only needs the config/ directory to exist. The Dart-side gateway_service.dart still calls setupDirs() before writeResolv() for the upgrade/restart case where directories may have been cleared by Android. Co-Authored-By: Mithun Gowda B <mithungowda.b7411@gmail.com> * Call setupDirectories in GatewayService before proot starts (#40) After an app update, Android may clear all files. The background GatewayService only called writeResolvConf() which creates config/ dir, but proot also needs tmp/, home/, lib/libtalloc.so.2, and proc/sys fakes for bind mounts. Now setupDirectories() runs before writeResolvConf() in the Android service, matching the Dart-side fix in gateway_service.dart. All paths now covered: - First-time setup: bootstrap calls setupDirs then writeResolv - App open (Dart): init/start call setupDirs then writeResolv - Background service (Kotlin): setupDirectories then writeResolvConf - writeResolvConf itself: mkdirs config dir as safety net Co-Authored-By: Mithun Gowda B <mithungowda.b7411@gmail.com> * Rewrite SSH to use native foreground service for persistent sshd The previous implementation used single-shot runInProot() which can't run daemons — proot uses --kill-on-exit so sshd dies when the command finishes. Also pgrep/hostname -I don't work reliably inside proot. Changes: - Add SshForegroundService.kt: runs sshd -D (foreground mode) in a persistent proot process with wake lock and notification - Add native bridge methods: startSshd, stopSshd, isSshdRunning, getSshdPort, getDeviceIps, setRootPassword - getDeviceIps uses Android NetworkInterface (not proot hostname -I) - setRootPassword runs chpasswd via processManager.runInProotSync - Register SshForegroundService in AndroidManifest.xml - Rewrite ssh_service.dart to use native methods - Update ssh_screen.dart to fetch IPs from Android Co-Authored-By: Mithun Gowda B <mithungowda.b7411@gmail.com> * Fix Ctrl key for soft keyboard input in terminal and configure screens CTRL/ALT toolbar buttons only worked with other toolbar buttons (arrows, etc.), not with soft keyboard input. Keyboard chars went directly through terminal.onOutput → pty.write(), bypassing the toolbar's modifier state. Fix: Share CTRL/ALT state via ValueNotifier between toolbar and screen. The onOutput handler now checks if modifiers are active and transforms keyboard input (e.g., Ctrl+C sends byte 0x03). Both terminal_screen.dart and configure_screen.dart are fixed. Co-Authored-By: Mithun Gowda B <mithungowda.b7411@gmail.com> * Fix Ctrl key in onboarding and package install screens, remove unused imports Add ctrlNotifier/altNotifier to TerminalToolbar usage in onboarding_screen.dart and package_install_screen.dart (build errors). Remove unnecessary dart:typed_data imports (already provided by package:flutter/services.dart). Co-Authored-By: Mithun Gowda B <mithungowda.b7411@gmail.com> * Fix resolv.conf ENOENT: use context.filesDir, make init() resilient (#40) - writeResolvConf() now uses context.filesDir (Android-guaranteed path) instead of the String configDir which could point to a cleared directory - gateway_service init() wraps writeResolv() in try-catch since it's non-critical when gateway is already running Co-Authored-By: Mithun Gowda B <mithungowda.b7411@gmail.com> * Run setupDirs + writeResolv unconditionally on app open (#40) Previously these only ran when the gateway was already running. After an app update, the gateway is not running but Android may have cleared the files directory. Moving setupDirs/writeResolv before the isGatewayRunning check ensures resolv.conf always exists before any terminal or gateway operation. Co-Authored-By: Mithun Gowda B <mithungowda.b7411@gmail.com> * Ensure dirs + resolv.conf exist before every proot operation (#40) getProotShellConfig() is called by all screens before starting proot (terminal, configure, onboarding, package install). Adding setupDirs and writeResolv here guarantees resolv.conf exists regardless of which screen the user opens first after an app update. Co-Authored-By: Mithun Gowda B <mithungowda.b7411@gmail.com> * Ensure dirs + resolv.conf on every app launch via splash screen (#40) The splash screen is the first screen on every app open — reinstall, update, or normal launch. Running setupDirs + writeResolv here guarantees the files exist before any screen can use proot. Co-Authored-By: Mithun Gowda B <mithungowda.b7411@gmail.com> * v1.8.0: AI Providers, SSH Access, Ctrl keys, Configure menu, resolv.conf fix - Bump version to 1.8.0 (pubspec.yaml, constants.dart) - Update CHANGELOG with all new features and bug fixes - Update README with new features, file structure, and OpenSSH package Co-Authored-By: Mithun Gowda B <mithungowda.b7411@gmail.com>
1 parent 58ce979 commit aaa8d0b

28 files changed

+2126
-40
lines changed

CHANGELOG.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,26 @@
11
# Changelog
22

3+
## v1.8.0 — AI Providers, SSH Access, Ctrl Keys & Configure Menu
4+
5+
### New Features
6+
7+
- **AI Providers** — New "AI Providers" screen to configure API keys and select models for 7 providers: Anthropic, OpenAI, Google Gemini, OpenRouter, NVIDIA NIM, DeepSeek, and xAI. Writes configuration directly to `~/.openclaw/openclaw.json`
8+
- **SSH Remote Access** — New "SSH Access" screen to start/stop an SSH server (sshd) inside proot, set the root password, and view connection info with copyable `ssh` commands. Runs as an Android foreground service for persistence
9+
- **Configure Menu** — New "Configure" dashboard card opens `openclaw configure` in a built-in terminal for managing gateway settings
10+
- **Clickable URLs** — Terminal and onboarding screens detect URLs at tap position (joining adjacent lines, stripping box-drawing characters) and offer Open/Copy/Cancel dialog
11+
12+
### Bug Fixes
13+
14+
- **Ctrl Key with Soft Keyboard (#37)** — Ctrl and Alt modifier state from the toolbar now applies to soft keyboard input across all terminal screens (terminal, configure, onboarding, package install). Previously only worked with toolbar buttons
15+
- **Ctrl+Arrow/Home/End/PgUp/PgDn (#38)** — Toolbar Ctrl modifier now sends correct escape sequences for arrow keys and navigation keys (e.g. `Ctrl+Left` sends `ESC[1;5D`)
16+
- **resolv.conf ENOENT after Update (#40)** — DNS resolution failed after app update because `resolv.conf` was missing. Now ensured on every app launch (splash screen), before every proot operation (`getProotShellConfig`), and in the gateway service init — covering reinstall, update, and normal launch
17+
18+
### Dashboard
19+
20+
- Added "AI Providers" and "SSH Access" quick action cards
21+
22+
---
23+
324
## v1.7.3 — DNS Fix, Snapshot & Version Sync
425

526
### Bug Fixes

README.md

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,12 +56,15 @@ OpenClaw brings the [OpenClaw](https://github.com/anthropics/openclaw) AI gatewa
5656
- **One-Tap Setup** — Downloads Ubuntu rootfs, Node.js 22, and OpenClaw automatically
5757
- **Built-in Terminal** — Full terminal emulator with extra keys toolbar, copy/paste, clickable URLs
5858
- **Gateway Controls** — Start/stop gateway with status indicator and health checks
59+
- **AI Providers** — Configure API keys and select models for 7 providers (Anthropic, OpenAI, Google Gemini, OpenRouter, NVIDIA NIM, DeepSeek, xAI)
60+
- **SSH Remote Access** — Start/stop SSH server, set root password, view connection info with copyable commands
61+
- **Configure Menu** — Run `openclaw configure` in a built-in terminal to manage gateway settings
5962
- **Node Device Capabilities** — 7 capabilities (15 commands) exposed to AI via WebSocket node protocol
6063
- **Token URL Display** — Captures auth token from onboarding, shows it with a copy button
6164
- **Web Dashboard** — Embedded WebView loads the dashboard with authentication token
6265
- **View Logs** — Real-time gateway log viewer with search/filter
6366
- **Onboarding** — Configure API keys and binding directly in-app
64-
- **Optional Packages** — Install Go (Golang) and Homebrew as optional dev tools from the setup wizard or dashboard
67+
- **Optional Packages** — Install Go (Golang), Homebrew, and OpenSSH as optional dev tools
6568
- **Settings** — Auto-start, battery optimization, system info, package status, re-run setup
6669
- **Foreground Service** — Keeps the gateway alive in the background with uptime tracking
6770
- **Setup Notifications** — Progress bar notifications during environment setup
@@ -74,6 +77,7 @@ After the initial setup completes, you can optionally install development tools
7477
|---------|---------------|------|
7578
| **Go (Golang)** | `apt install golang` | ~150 MB |
7679
| **Homebrew** | Official installer (with root workaround) | ~500 MB |
80+
| **OpenSSH** | `apt install openssh-server` | ~10 MB |
7781

7882
These are accessible from:
7983
- **Setup Wizard** — Package cards appear after setup completes
@@ -222,7 +226,8 @@ flutter_app/lib/
222226
│ ├── node_state.dart # Node connection status
223227
│ ├── node_frame.dart # WebSocket frame model (req/res/event)
224228
│ ├── setup_state.dart # Setup wizard progress
225-
│ └── optional_package.dart # Optional package metadata (Go, Homebrew)
229+
│ ├── optional_package.dart # Optional package metadata (Go, Homebrew)
230+
│ └── ai_provider.dart # AI provider data model (7 providers)
226231
├── providers/
227232
│ ├── gateway_provider.dart # Gateway state management
228233
│ ├── node_provider.dart # Node capabilities + permission management
@@ -233,7 +238,11 @@ flutter_app/lib/
233238
│ ├── onboarding_screen.dart # API key configuration terminal
234239
│ ├── dashboard_screen.dart # Main dashboard with quick actions
235240
│ ├── terminal_screen.dart # Full terminal emulator
241+
│ ├── configure_screen.dart # openclaw configure terminal
236242
│ ├── web_dashboard_screen.dart # WebView for OpenClaw dashboard
243+
│ ├── providers_screen.dart # AI provider list
244+
│ ├── provider_detail_screen.dart # API key + model configuration
245+
│ ├── ssh_screen.dart # SSH server management
237246
│ ├── packages_screen.dart # Optional package manager
238247
│ ├── package_install_screen.dart # Terminal-based package installer
239248
│ ├── logs_screen.dart # Gateway log viewer
@@ -248,6 +257,8 @@ flutter_app/lib/
248257
│ ├── bootstrap_service.dart # Environment setup orchestration
249258
│ ├── package_service.dart # Optional package status checking
250259
│ ├── preferences_service.dart # Persistent settings (token URL, etc.)
260+
│ ├── provider_config_service.dart # AI provider config read/write
261+
│ ├── ssh_service.dart # SSH server management via native bridge
251262
│ └── capabilities/
252263
│ ├── capability_handler.dart # Base class with permission handling
253264
│ ├── camera_capability.dart # Photo/video capture

flutter_app/android/app/src/main/AndroidManifest.xml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,11 @@
6666
android:exported="false"
6767
android:foregroundServiceType="specialUse" />
6868

69+
<service
70+
android:name=".SshForegroundService"
71+
android:exported="false"
72+
android:foregroundServiceType="specialUse" />
73+
6974
<service
7075
android:name=".ScreenCaptureService"
7176
android:exported="false"

flutter_app/android/app/src/main/kotlin/com/nxg/openclawproot/BootstrapManager.kt

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1213,10 +1213,16 @@ require('/root/.openclaw/proot-compat.js');
12131213
}
12141214

12151215
fun writeResolvConf() {
1216-
val configDir = File(this.configDir)
1217-
configDir.mkdirs()
1218-
1219-
File("$configDir/resolv.conf").writeText("nameserver 8.8.8.8\nnameserver 8.8.4.4\n")
1216+
// Ensure the config directory exists. Use context.filesDir as the
1217+
// canonical base (Android always ensures it exists) rather than
1218+
// relying solely on the String path, which may point to a directory
1219+
// that was cleared during an app upgrade. (#40)
1220+
val dir = File(context.filesDir, "config")
1221+
if (!dir.exists()) {
1222+
dir.mkdirs()
1223+
}
1224+
val resolv = File(dir, "resolv.conf")
1225+
resolv.writeText("nameserver 8.8.8.8\nnameserver 8.8.4.4\n")
12201226
}
12211227

12221228
/** Read a file from inside the rootfs (e.g. /root/.openclaw/openclaw.json). */

flutter_app/android/app/src/main/kotlin/com/nxg/openclawproot/GatewayService.kt

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -80,9 +80,11 @@ class GatewayService : Service() {
8080
val nativeLibDir = applicationContext.applicationInfo.nativeLibraryDir
8181
val pm = ProcessManager(filesDir, nativeLibDir)
8282

83-
// Refresh resolv.conf before every gateway start so DNS always works,
84-
// even after Android clears the app's file cache.
83+
// Recreate all directories (config, tmp, home, lib, proc/sys fakes)
84+
// in case Android cleared them after an app update (#40).
85+
// This must run before proot — it needs bind-mount targets.
8586
val bootstrapManager = BootstrapManager(applicationContext, filesDir, nativeLibDir)
87+
bootstrapManager.setupDirectories()
8688
bootstrapManager.writeResolvConf()
8789

8890
gatewayProcess = pm.startProotProcess("openclaw gateway --verbose")

flutter_app/android/app/src/main/kotlin/com/nxg/openclawproot/MainActivity.kt

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,50 @@ class MainActivity : FlutterActivity() {
162162
NodeForegroundService.updateStatus(text)
163163
result.success(true)
164164
}
165+
"startSshd" -> {
166+
val port = call.argument<Int>("port") ?: 8022
167+
try {
168+
SshForegroundService.start(applicationContext, port)
169+
result.success(true)
170+
} catch (e: Exception) {
171+
result.error("SERVICE_ERROR", e.message, null)
172+
}
173+
}
174+
"stopSshd" -> {
175+
try {
176+
SshForegroundService.stop(applicationContext)
177+
result.success(true)
178+
} catch (e: Exception) {
179+
result.error("SERVICE_ERROR", e.message, null)
180+
}
181+
}
182+
"isSshdRunning" -> {
183+
result.success(SshForegroundService.isRunning)
184+
}
185+
"getSshdPort" -> {
186+
result.success(SshForegroundService.currentPort)
187+
}
188+
"getDeviceIps" -> {
189+
result.success(SshForegroundService.getDeviceIps())
190+
}
191+
"setRootPassword" -> {
192+
val password = call.argument<String>("password")
193+
if (password != null) {
194+
Thread {
195+
try {
196+
val escaped = password.replace("'", "'\\''")
197+
processManager.runInProotSync(
198+
"echo 'root:$escaped' | chpasswd", 15
199+
)
200+
runOnUiThread { result.success(true) }
201+
} catch (e: Exception) {
202+
runOnUiThread { result.error("SSH_ERROR", e.message, null) }
203+
}
204+
}.start()
205+
} else {
206+
result.error("INVALID_ARGS", "password required", null)
207+
}
208+
}
165209
"requestBatteryOptimization" -> {
166210
try {
167211
val intent = Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS).apply {
Lines changed: 213 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,213 @@
1+
package com.nxg.openclawproot
2+
3+
import android.app.Notification
4+
import android.app.NotificationChannel
5+
import android.app.NotificationManager
6+
import android.app.PendingIntent
7+
import android.app.Service
8+
import android.content.Context
9+
import android.content.Intent
10+
import android.os.Build
11+
import android.os.IBinder
12+
import android.os.PowerManager
13+
import java.io.BufferedReader
14+
import java.io.InputStreamReader
15+
import java.net.NetworkInterface
16+
17+
/**
18+
* Foreground service that runs sshd in a persistent proot process.
19+
* sshd must run with -D (no daemonize) so proot stays alive.
20+
*/
21+
class SshForegroundService : Service() {
22+
companion object {
23+
const val CHANNEL_ID = "openclaw_ssh"
24+
const val NOTIFICATION_ID = 5
25+
const val EXTRA_PORT = "port"
26+
var isRunning = false
27+
private set
28+
var currentPort = 8022
29+
private set
30+
private var instance: SshForegroundService? = null
31+
32+
fun start(context: Context, port: Int = 8022) {
33+
val intent = Intent(context, SshForegroundService::class.java).apply {
34+
putExtra(EXTRA_PORT, port)
35+
}
36+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
37+
context.startForegroundService(intent)
38+
} else {
39+
context.startService(intent)
40+
}
41+
}
42+
43+
fun stop(context: Context) {
44+
val intent = Intent(context, SshForegroundService::class.java)
45+
context.stopService(intent)
46+
}
47+
48+
/** Get device IP addresses via Android NetworkInterface. */
49+
fun getDeviceIps(): List<String> {
50+
val ips = mutableListOf<String>()
51+
try {
52+
val interfaces = NetworkInterface.getNetworkInterfaces() ?: return ips
53+
for (iface in interfaces) {
54+
if (iface.isLoopback || !iface.isUp) continue
55+
for (addr in iface.inetAddresses) {
56+
if (addr.isLoopbackAddress) continue
57+
val hostAddr = addr.hostAddress ?: continue
58+
// Skip IPv6 link-local
59+
if (hostAddr.contains("%")) continue
60+
// Prefer IPv4, but include IPv6 too
61+
ips.add(hostAddr)
62+
}
63+
}
64+
} catch (_: Exception) {}
65+
return ips
66+
}
67+
}
68+
69+
private var sshdProcess: Process? = null
70+
private var wakeLock: PowerManager.WakeLock? = null
71+
72+
override fun onBind(intent: Intent?): IBinder? = null
73+
74+
override fun onCreate() {
75+
super.onCreate()
76+
createNotificationChannel()
77+
}
78+
79+
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
80+
val port = intent?.getIntExtra(EXTRA_PORT, 8022) ?: 8022
81+
currentPort = port
82+
startForeground(NOTIFICATION_ID, buildNotification("Starting SSH on port $port..."))
83+
acquireWakeLock()
84+
startSshd(port)
85+
return START_STICKY
86+
}
87+
88+
override fun onDestroy() {
89+
isRunning = false
90+
instance = null
91+
stopSshd()
92+
releaseWakeLock()
93+
super.onDestroy()
94+
}
95+
96+
private fun startSshd(port: Int) {
97+
isRunning = true
98+
instance = this
99+
100+
Thread {
101+
try {
102+
val filesDir = applicationContext.filesDir.absolutePath
103+
val nativeLibDir = applicationContext.applicationInfo.nativeLibraryDir
104+
val pm = ProcessManager(filesDir, nativeLibDir)
105+
106+
// Ensure directories exist
107+
val bootstrapManager = BootstrapManager(applicationContext, filesDir, nativeLibDir)
108+
bootstrapManager.setupDirectories()
109+
bootstrapManager.writeResolvConf()
110+
111+
// Generate host keys if missing, configure sshd, then run in
112+
// foreground mode (-D) so the proot process stays alive.
113+
// -e logs to stderr instead of syslog (easier debugging).
114+
// PermitRootLogin yes is needed since proot fakes root.
115+
val cmd = "mkdir -p /run/sshd /etc/ssh && " +
116+
"test -f /etc/ssh/ssh_host_rsa_key || ssh-keygen -A && " +
117+
"sed -i 's/^#\\?PermitRootLogin.*/PermitRootLogin yes/' /etc/ssh/sshd_config 2>/dev/null; " +
118+
"grep -q '^PermitRootLogin' /etc/ssh/sshd_config || echo 'PermitRootLogin yes' >> /etc/ssh/sshd_config; " +
119+
"/usr/sbin/sshd -D -e -p $port"
120+
121+
sshdProcess = pm.startProotProcess(cmd)
122+
updateNotification("SSH running on port $port")
123+
124+
// Read stderr for logs
125+
val stderrReader = BufferedReader(InputStreamReader(sshdProcess!!.errorStream))
126+
Thread {
127+
try {
128+
var line: String?
129+
while (stderrReader.readLine().also { line = it } != null) {
130+
// Could log these if needed
131+
}
132+
} catch (_: Exception) {}
133+
}.start()
134+
135+
val exitCode = sshdProcess!!.waitFor()
136+
isRunning = false
137+
updateNotification("SSH stopped (exit $exitCode)")
138+
stopSelf()
139+
} catch (e: Exception) {
140+
isRunning = false
141+
updateNotification("SSH error: ${e.message?.take(50)}")
142+
stopSelf()
143+
}
144+
}.start()
145+
}
146+
147+
private fun stopSshd() {
148+
sshdProcess?.let {
149+
it.destroyForcibly()
150+
sshdProcess = null
151+
}
152+
}
153+
154+
private fun acquireWakeLock() {
155+
val powerManager = getSystemService(Context.POWER_SERVICE) as PowerManager
156+
wakeLock = powerManager.newWakeLock(
157+
PowerManager.PARTIAL_WAKE_LOCK,
158+
"OpenClaw::SshWakeLock"
159+
)
160+
wakeLock?.acquire(24 * 60 * 60 * 1000L)
161+
}
162+
163+
private fun releaseWakeLock() {
164+
wakeLock?.let {
165+
if (it.isHeld) it.release()
166+
}
167+
wakeLock = null
168+
}
169+
170+
private fun createNotificationChannel() {
171+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
172+
val channel = NotificationChannel(
173+
CHANNEL_ID,
174+
"OpenClaw SSH",
175+
NotificationManager.IMPORTANCE_LOW
176+
).apply {
177+
description = "Keeps the SSH server running in the background"
178+
}
179+
val manager = getSystemService(NotificationManager::class.java)
180+
manager.createNotificationChannel(channel)
181+
}
182+
}
183+
184+
private fun buildNotification(text: String): Notification {
185+
val intent = Intent(this, MainActivity::class.java)
186+
val pendingIntent = PendingIntent.getActivity(
187+
this, 0, intent,
188+
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
189+
)
190+
191+
val builder = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
192+
Notification.Builder(this, CHANNEL_ID)
193+
} else {
194+
@Suppress("DEPRECATION")
195+
Notification.Builder(this)
196+
}
197+
198+
builder.setContentTitle("OpenClaw SSH")
199+
.setContentText(text)
200+
.setSmallIcon(android.R.drawable.ic_lock_lock)
201+
.setContentIntent(pendingIntent)
202+
.setOngoing(true)
203+
204+
return builder.build()
205+
}
206+
207+
private fun updateNotification(text: String) {
208+
try {
209+
val manager = getSystemService(NotificationManager::class.java)
210+
manager.notify(NOTIFICATION_ID, buildNotification(text))
211+
} catch (_: Exception) {}
212+
}
213+
}

0 commit comments

Comments
 (0)