Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,19 @@
# Changelog

## v1.7.3 — DNS Fix, Snapshot & Version Sync

### Bug Fixes

- **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
- **Version Mismatch (#35)** — Synced version strings across `constants.dart`, `pubspec.yaml`, `package.json`, and `lib/index.js` so they all report `1.7.3`

### New Features

- **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
- **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

---

## v1.7.2 — Setup Fix

### Bug Fixes
Expand Down
7 changes: 6 additions & 1 deletion flutter_app/android/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">

<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
Expand All @@ -15,12 +16,16 @@
<uses-permission android:name="android.permission.VIBRATE" />
<uses-permission android:name="android.permission.BODY_SENSORS" />
<uses-permission android:name="android.permission.HIGH_SAMPLING_RATE_SENSORS" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="32" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="29" tools:replace="android:maxSdkVersion" />
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />

<application
android:label="OpenClaw"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher"
android:usesCleartextTraffic="true"
android:requestLegacyExternalStorage="true"
android:extractNativeLibs="true">

<activity
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1219,6 +1219,19 @@ require('/root/.openclaw/proot-compat.js');
File("$configDir/resolv.conf").writeText("nameserver 8.8.8.8\nnameserver 8.8.4.4\n")
}

/** Read a file from inside the rootfs (e.g. /root/.openclaw/openclaw.json). */
fun readRootfsFile(path: String): String? {
val file = File("$rootfsDir/$path")
return if (file.exists()) file.readText() else null
}

/** Write content to a file inside the rootfs, creating parent dirs as needed. */
fun writeRootfsFile(path: String, content: String) {
val file = File("$rootfsDir/$path")
file.parentFile?.mkdirs()
file.writeText(content)
}

/**
* Create fake /proc and /sys files that are bind-mounted into proot.
* Android restricts access to many /proc entries; proot-distro works
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,11 @@ class GatewayService : Service() {
val nativeLibDir = applicationContext.applicationInfo.nativeLibraryDir
val pm = ProcessManager(filesDir, nativeLibDir)

// Refresh resolv.conf before every gateway start so DNS always works,
// even after Android clears the app's file cache.
val bootstrapManager = BootstrapManager(applicationContext, filesDir, nativeLibDir)
bootstrapManager.writeResolvConf()

gatewayProcess = pm.startProotProcess("openclaw gateway --verbose")
updateNotificationRunning()
emitLog("Gateway started")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import android.app.Activity
import android.content.Context
import android.os.Environment
import android.hardware.Sensor
import android.hardware.SensorEvent
import android.hardware.SensorEventListener
Expand Down Expand Up @@ -339,6 +340,72 @@ class MainActivity : FlutterActivity() {
result.error("VIBRATE_ERROR", e.message, null)
}
}
"requestStoragePermission" -> {
try {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
// Android 11+: MANAGE_EXTERNAL_STORAGE
if (!Environment.isExternalStorageManager()) {
val intent = Intent(Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION)
startActivity(intent)
}
} else {
// Android 10 and below: READ/WRITE_EXTERNAL_STORAGE
ActivityCompat.requestPermissions(
this,
arrayOf(
Manifest.permission.READ_EXTERNAL_STORAGE,
Manifest.permission.WRITE_EXTERNAL_STORAGE
),
STORAGE_PERMISSION_REQUEST
)
}
result.success(true)
} catch (e: Exception) {
result.error("STORAGE_ERROR", e.message, null)
}
}
"hasStoragePermission" -> {
val hasPermission = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
Environment.isExternalStorageManager()
} else {
ContextCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED
}
result.success(hasPermission)
}
"getExternalStoragePath" -> {
result.success(Environment.getExternalStorageDirectory().absolutePath)
}
"readRootfsFile" -> {
val path = call.argument<String>("path")
if (path != null) {
Thread {
try {
val content = bootstrapManager.readRootfsFile(path)
runOnUiThread { result.success(content) }
} catch (e: Exception) {
runOnUiThread { result.error("ROOTFS_READ_ERROR", e.message, null) }
}
}.start()
} else {
result.error("INVALID_ARGS", "path required", null)
}
}
"writeRootfsFile" -> {
val path = call.argument<String>("path")
val content = call.argument<String>("content")
if (path != null && content != null) {
Thread {
try {
bootstrapManager.writeRootfsFile(path, content)
runOnUiThread { result.success(true) }
} catch (e: Exception) {
runOnUiThread { result.error("ROOTFS_WRITE_ERROR", e.message, null) }
}
}.start()
} else {
result.error("INVALID_ARGS", "path and content required", null)
}
}
"readSensor" -> {
val sensorType = call.argument<String>("sensor") ?: "accelerometer"
Thread {
Expand Down Expand Up @@ -408,6 +475,7 @@ class MainActivity : FlutterActivity() {

createUrlNotificationChannel()
requestNotificationPermission()
requestStoragePermissionOnLaunch()

EventChannel(flutterEngine.dartExecutor.binaryMessenger, EVENT_CHANNEL).setStreamHandler(
object : EventChannel.StreamHandler {
Expand Down Expand Up @@ -435,6 +503,30 @@ class MainActivity : FlutterActivity() {
}
}

private fun requestStoragePermissionOnLaunch() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
if (!Environment.isExternalStorageManager()) {
try {
val intent = Intent(Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION)
startActivity(intent)
} catch (_: Exception) {}
}
} else {
if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE)
!= PackageManager.PERMISSION_GRANTED
) {
ActivityCompat.requestPermissions(
this,
arrayOf(
Manifest.permission.READ_EXTERNAL_STORAGE,
Manifest.permission.WRITE_EXTERNAL_STORAGE
),
STORAGE_PERMISSION_REQUEST
)
}
}
}

private fun createUrlNotificationChannel() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val channel = NotificationChannel(
Expand Down Expand Up @@ -522,5 +614,6 @@ class MainActivity : FlutterActivity() {
const val URL_CHANNEL_ID = "openclaw_urls"
const val NOTIFICATION_PERMISSION_REQUEST = 1001
const val SCREEN_CAPTURE_REQUEST = 1002
const val STORAGE_PERMISSION_REQUEST = 1003
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
package com.nxg.openclawproot

import android.os.Build
import android.os.Environment
import java.io.BufferedReader
import java.io.File
import java.io.InputStreamReader

/**
Expand Down Expand Up @@ -93,7 +96,40 @@ class ProcessManager(
// App-specific binds
"--bind=$configDir/resolv.conf:/etc/resolv.conf",
"--bind=$homeDir:/root/home",
)
).let { flags ->
// Bind-mount shared storage into proot (Termux proot-distro style).
// Bind the whole /storage tree so symlinks and sub-mounts resolve.
// Then create /sdcard symlink inside rootfs pointing to the right path.
val hasAccess = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
Environment.isExternalStorageManager()
} else {
val sdcard = Environment.getExternalStorageDirectory()
sdcard.exists() && sdcard.canRead()
}

if (hasAccess) {
val storageDir = File("$rootfsDir/storage")
storageDir.mkdirs()
// Create /sdcard symlink → /storage/emulated/0 inside rootfs
val sdcardLink = File("$rootfsDir/sdcard")
if (!sdcardLink.exists()) {
try {
Runtime.getRuntime().exec(
arrayOf("ln", "-sf", "/storage/emulated/0", "$rootfsDir/sdcard")
).waitFor()
} catch (_: Exception) {
// Fallback: create as directory if symlink fails
sdcardLink.mkdirs()
}
}
flags + listOf(
"--bind=/storage:/storage",
"--bind=/storage/emulated/0:/sdcard"
)
} else {
flags
}
}
}

// ================================================================
Expand Down
2 changes: 1 addition & 1 deletion flutter_app/lib/constants.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
class AppConstants {
static const String appName = 'OpenClaw';
static const String version = '1.7.1';
static const String version = '1.7.3';
static const String packageName = 'com.nxg.openclawproot';

/// Matches ANSI escape sequences (e.g. color codes in terminal output).
Expand Down
9 changes: 9 additions & 0 deletions flutter_app/lib/screens/dashboard_screen.dart
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,15 @@ class DashboardScreen extends StatelessWidget {
MaterialPageRoute(builder: (_) => const LogsScreen()),
),
),
StatusCard(
title: 'Snapshot',
subtitle: 'Backup or restore your config',
icon: Icons.backup,
trailing: const Icon(Icons.chevron_right),
onTap: () => Navigator.of(context).push(
MaterialPageRoute(builder: (_) => const SettingsScreen()),
),
),
Consumer<NodeProvider>(
builder: (context, nodeProvider, _) {
final nodeState = nodeProvider.state;
Expand Down
Loading
Loading