Skip to content

Commit d45e3ca

Browse files
authored
v1.7.3: Fix DNS reliability, add snapshot backup & storage access (#36)
* v1.7.3: Fix DNS reliability, add snapshot backup & storage access - Fix #34: Write resolv.conf before every gateway start (Flutter + Android foreground service) so DNS never breaks from cache clearing - Fix #35: Sync version strings across constants.dart, pubspec.yaml, package.json, and lib/index.js to 1.7.3 - Fix #27: Add config snapshot export/import under Settings > Maintenance, with dashboard quick action card - Add Termux-style storage access: request MANAGE_EXTERNAL_STORAGE, bind-mount /sdcard into proot, save snapshots to /sdcard/Download/ - Bump version to 1.7.3+10 Co-Authored-By: Mithun Gowda B <mithungowda.b7411@gmail.com> * Fix manifest merger conflict with camera_android_camerax Add tools namespace and tools:replace="android:maxSdkVersion" on WRITE_EXTERNAL_STORAGE to override the camera library's maxSdkVersion=28 with our maxSdkVersion=29. Co-Authored-By: Mithun Gowda B <mithungowda.b7411@gmail.com> * Fix empty /sdcard in proot: bind /storage tree and check permission properly - Bind the whole /storage directory (not just /storage/emulated/0) so Android's FUSE symlinks and sub-mounts resolve correctly inside proot - Check Environment.isExternalStorageManager() on Android 11+ instead of just canRead(), which returns true even without actual file access - Create /sdcard symlink inside rootfs pointing to /storage/emulated/0 - Bind both /storage:/storage and /storage/emulated/0:/sdcard for maximum compatibility (matches Termux proot-distro behavior) Co-Authored-By: Mithun Gowda B <mithungowda.b7411@gmail.com> * Request storage permission on app launch Automatically prompts for "All files access" (Android 11+) or READ/WRITE_EXTERNAL_STORAGE (older) when the app starts, so /sdcard is accessible in proot without needing to manually go to Settings. Co-Authored-By: Mithun Gowda B <mithungowda.b7411@gmail.com> * Add /sdcard bind mount to terminal proot session The terminal builds its own proot args in Dart (terminal_service.dart) separately from ProcessManager.kt. Added /storage and /sdcard bind mounts to buildProotArgs() so the terminal shell also has access to shared storage when permission is granted. Co-Authored-By: Mithun Gowda B <mithungowda.b7411@gmail.com>
1 parent 2e6e69f commit d45e3ca

File tree

15 files changed

+352
-8
lines changed

15 files changed

+352
-8
lines changed

CHANGELOG.md

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

3+
## v1.7.3 — DNS Fix, Snapshot & Version Sync
4+
5+
### Bug Fixes
6+
7+
- **DNS Breaks After a While (#34)**`resolv.conf` is now written before every gateway start (in both the Flutter service and the Android foreground service), not just during initial setup. This prevents DNS resolution failures when Android clears the app's file cache
8+
- **Version Mismatch (#35)** — Synced version strings across `constants.dart`, `pubspec.yaml`, `package.json`, and `lib/index.js` so they all report `1.7.3`
9+
10+
### New Features
11+
12+
- **Config Snapshot (#27)** — Added Export/Import Snapshot buttons under Settings > Maintenance. Export saves `openclaw.json` and app preferences to a JSON file; Import restores them. A "Snapshot" quick action card is also available on the dashboard
13+
- **Storage Access** — Added Termux-style "Setup Storage" in Settings. Grants shared storage permission and bind-mounts `/sdcard` into proot, so files in `/sdcard/Download` (etc.) are accessible from inside the Ubuntu environment. Snapshots are saved to `/sdcard/Download/` when permission is granted
14+
15+
---
16+
317
## v1.7.2 — Setup Fix
418

519
### Bug Fixes

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

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
1+
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
2+
xmlns:tools="http://schemas.android.com/tools">
23

34
<uses-permission android:name="android.permission.INTERNET" />
45
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
@@ -15,12 +16,16 @@
1516
<uses-permission android:name="android.permission.VIBRATE" />
1617
<uses-permission android:name="android.permission.BODY_SENSORS" />
1718
<uses-permission android:name="android.permission.HIGH_SAMPLING_RATE_SENSORS" />
19+
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="32" />
20+
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="29" tools:replace="android:maxSdkVersion" />
21+
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />
1822

1923
<application
2024
android:label="OpenClaw"
2125
android:name="${applicationName}"
2226
android:icon="@mipmap/ic_launcher"
2327
android:usesCleartextTraffic="true"
28+
android:requestLegacyExternalStorage="true"
2429
android:extractNativeLibs="true">
2530

2631
<activity

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

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1219,6 +1219,19 @@ require('/root/.openclaw/proot-compat.js');
12191219
File("$configDir/resolv.conf").writeText("nameserver 8.8.8.8\nnameserver 8.8.4.4\n")
12201220
}
12211221

1222+
/** Read a file from inside the rootfs (e.g. /root/.openclaw/openclaw.json). */
1223+
fun readRootfsFile(path: String): String? {
1224+
val file = File("$rootfsDir/$path")
1225+
return if (file.exists()) file.readText() else null
1226+
}
1227+
1228+
/** Write content to a file inside the rootfs, creating parent dirs as needed. */
1229+
fun writeRootfsFile(path: String, content: String) {
1230+
val file = File("$rootfsDir/$path")
1231+
file.parentFile?.mkdirs()
1232+
file.writeText(content)
1233+
}
1234+
12221235
/**
12231236
* Create fake /proc and /sys files that are bind-mounted into proot.
12241237
* Android restricts access to many /proc entries; proot-distro works

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +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.
85+
val bootstrapManager = BootstrapManager(applicationContext, filesDir, nativeLibDir)
86+
bootstrapManager.writeResolvConf()
87+
8388
gatewayProcess = pm.startProotProcess("openclaw gateway --verbose")
8489
updateNotificationRunning()
8590
emitLog("Gateway started")

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

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import androidx.core.app.ActivityCompat
1717
import androidx.core.content.ContextCompat
1818
import android.app.Activity
1919
import android.content.Context
20+
import android.os.Environment
2021
import android.hardware.Sensor
2122
import android.hardware.SensorEvent
2223
import android.hardware.SensorEventListener
@@ -339,6 +340,72 @@ class MainActivity : FlutterActivity() {
339340
result.error("VIBRATE_ERROR", e.message, null)
340341
}
341342
}
343+
"requestStoragePermission" -> {
344+
try {
345+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
346+
// Android 11+: MANAGE_EXTERNAL_STORAGE
347+
if (!Environment.isExternalStorageManager()) {
348+
val intent = Intent(Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION)
349+
startActivity(intent)
350+
}
351+
} else {
352+
// Android 10 and below: READ/WRITE_EXTERNAL_STORAGE
353+
ActivityCompat.requestPermissions(
354+
this,
355+
arrayOf(
356+
Manifest.permission.READ_EXTERNAL_STORAGE,
357+
Manifest.permission.WRITE_EXTERNAL_STORAGE
358+
),
359+
STORAGE_PERMISSION_REQUEST
360+
)
361+
}
362+
result.success(true)
363+
} catch (e: Exception) {
364+
result.error("STORAGE_ERROR", e.message, null)
365+
}
366+
}
367+
"hasStoragePermission" -> {
368+
val hasPermission = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
369+
Environment.isExternalStorageManager()
370+
} else {
371+
ContextCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED
372+
}
373+
result.success(hasPermission)
374+
}
375+
"getExternalStoragePath" -> {
376+
result.success(Environment.getExternalStorageDirectory().absolutePath)
377+
}
378+
"readRootfsFile" -> {
379+
val path = call.argument<String>("path")
380+
if (path != null) {
381+
Thread {
382+
try {
383+
val content = bootstrapManager.readRootfsFile(path)
384+
runOnUiThread { result.success(content) }
385+
} catch (e: Exception) {
386+
runOnUiThread { result.error("ROOTFS_READ_ERROR", e.message, null) }
387+
}
388+
}.start()
389+
} else {
390+
result.error("INVALID_ARGS", "path required", null)
391+
}
392+
}
393+
"writeRootfsFile" -> {
394+
val path = call.argument<String>("path")
395+
val content = call.argument<String>("content")
396+
if (path != null && content != null) {
397+
Thread {
398+
try {
399+
bootstrapManager.writeRootfsFile(path, content)
400+
runOnUiThread { result.success(true) }
401+
} catch (e: Exception) {
402+
runOnUiThread { result.error("ROOTFS_WRITE_ERROR", e.message, null) }
403+
}
404+
}.start()
405+
} else {
406+
result.error("INVALID_ARGS", "path and content required", null)
407+
}
408+
}
342409
"readSensor" -> {
343410
val sensorType = call.argument<String>("sensor") ?: "accelerometer"
344411
Thread {
@@ -408,6 +475,7 @@ class MainActivity : FlutterActivity() {
408475

409476
createUrlNotificationChannel()
410477
requestNotificationPermission()
478+
requestStoragePermissionOnLaunch()
411479

412480
EventChannel(flutterEngine.dartExecutor.binaryMessenger, EVENT_CHANNEL).setStreamHandler(
413481
object : EventChannel.StreamHandler {
@@ -435,6 +503,30 @@ class MainActivity : FlutterActivity() {
435503
}
436504
}
437505

506+
private fun requestStoragePermissionOnLaunch() {
507+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
508+
if (!Environment.isExternalStorageManager()) {
509+
try {
510+
val intent = Intent(Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION)
511+
startActivity(intent)
512+
} catch (_: Exception) {}
513+
}
514+
} else {
515+
if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE)
516+
!= PackageManager.PERMISSION_GRANTED
517+
) {
518+
ActivityCompat.requestPermissions(
519+
this,
520+
arrayOf(
521+
Manifest.permission.READ_EXTERNAL_STORAGE,
522+
Manifest.permission.WRITE_EXTERNAL_STORAGE
523+
),
524+
STORAGE_PERMISSION_REQUEST
525+
)
526+
}
527+
}
528+
}
529+
438530
private fun createUrlNotificationChannel() {
439531
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
440532
val channel = NotificationChannel(
@@ -522,5 +614,6 @@ class MainActivity : FlutterActivity() {
522614
const val URL_CHANNEL_ID = "openclaw_urls"
523615
const val NOTIFICATION_PERMISSION_REQUEST = 1001
524616
const val SCREEN_CAPTURE_REQUEST = 1002
617+
const val STORAGE_PERMISSION_REQUEST = 1003
525618
}
526619
}

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

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
package com.nxg.openclawproot
22

3+
import android.os.Build
4+
import android.os.Environment
35
import java.io.BufferedReader
6+
import java.io.File
47
import java.io.InputStreamReader
58

69
/**
@@ -93,7 +96,40 @@ class ProcessManager(
9396
// App-specific binds
9497
"--bind=$configDir/resolv.conf:/etc/resolv.conf",
9598
"--bind=$homeDir:/root/home",
96-
)
99+
).let { flags ->
100+
// Bind-mount shared storage into proot (Termux proot-distro style).
101+
// Bind the whole /storage tree so symlinks and sub-mounts resolve.
102+
// Then create /sdcard symlink inside rootfs pointing to the right path.
103+
val hasAccess = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
104+
Environment.isExternalStorageManager()
105+
} else {
106+
val sdcard = Environment.getExternalStorageDirectory()
107+
sdcard.exists() && sdcard.canRead()
108+
}
109+
110+
if (hasAccess) {
111+
val storageDir = File("$rootfsDir/storage")
112+
storageDir.mkdirs()
113+
// Create /sdcard symlink → /storage/emulated/0 inside rootfs
114+
val sdcardLink = File("$rootfsDir/sdcard")
115+
if (!sdcardLink.exists()) {
116+
try {
117+
Runtime.getRuntime().exec(
118+
arrayOf("ln", "-sf", "/storage/emulated/0", "$rootfsDir/sdcard")
119+
).waitFor()
120+
} catch (_: Exception) {
121+
// Fallback: create as directory if symlink fails
122+
sdcardLink.mkdirs()
123+
}
124+
}
125+
flags + listOf(
126+
"--bind=/storage:/storage",
127+
"--bind=/storage/emulated/0:/sdcard"
128+
)
129+
} else {
130+
flags
131+
}
132+
}
97133
}
98134

99135
// ================================================================

flutter_app/lib/constants.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
class AppConstants {
22
static const String appName = 'OpenClaw';
3-
static const String version = '1.7.1';
3+
static const String version = '1.7.3';
44
static const String packageName = 'com.nxg.openclawproot';
55

66
/// Matches ANSI escape sequences (e.g. color codes in terminal output).

flutter_app/lib/screens/dashboard_screen.dart

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,15 @@ class DashboardScreen extends StatelessWidget {
107107
MaterialPageRoute(builder: (_) => const LogsScreen()),
108108
),
109109
),
110+
StatusCard(
111+
title: 'Snapshot',
112+
subtitle: 'Backup or restore your config',
113+
icon: Icons.backup,
114+
trailing: const Icon(Icons.chevron_right),
115+
onTap: () => Navigator.of(context).push(
116+
MaterialPageRoute(builder: (_) => const SettingsScreen()),
117+
),
118+
),
110119
Consumer<NodeProvider>(
111120
builder: (context, nodeProvider, _) {
112121
final nodeState = nodeProvider.state;

0 commit comments

Comments
 (0)