diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index ea169d2..f262d7d 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -26,6 +26,11 @@
+
+
+
+
+
diff --git a/app/src/main/java/com/inou/clawdnode/gateway/DirectGateway.kt b/app/src/main/java/com/inou/clawdnode/gateway/DirectGateway.kt
index 06e4013..7593ee2 100644
--- a/app/src/main/java/com/inou/clawdnode/gateway/DirectGateway.kt
+++ b/app/src/main/java/com/inou/clawdnode/gateway/DirectGateway.kt
@@ -3,8 +3,10 @@ package com.inou.clawdnode.gateway
import android.util.Log
import com.inou.clawdnode.ClawdNodeApp
import com.inou.clawdnode.debug.DebugClient
+import com.inou.clawdnode.sms.SmsProvider
import kotlinx.coroutines.*
import okhttp3.*
+import org.json.JSONArray
import org.json.JSONObject
import java.util.concurrent.TimeUnit
@@ -232,6 +234,60 @@ object DirectGateway {
ClawdNodeApp.instance.auditLog.log("COMMAND_EXECUTED", "call.hangup")
}
+ "sms.list" -> {
+ val limit = params.optInt("limit", 50)
+ val offset = params.optInt("offset", 0)
+ val since = if (params.has("since")) params.optLong("since") else null
+ val type = if (params.has("type")) params.optInt("type") else null
+ val messages = SmsProvider.listMessages(limit, offset, since, type)
+ val arr = JSONArray()
+ messages.forEach { arr.put(it.toJson()) }
+ sendDataResponse(commandId, JSONObject().put("messages", arr))
+ ClawdNodeApp.instance.auditLog.log("COMMAND_EXECUTED", "sms.list")
+ }
+
+ "sms.read" -> {
+ val id = params.optLong("id")
+ val msg = SmsProvider.getMessage(id)
+ if (msg != null) {
+ sendDataResponse(commandId, msg.toJson())
+ } else {
+ sendResponse(commandId, false, "Message not found: $id")
+ }
+ ClawdNodeApp.instance.auditLog.log("COMMAND_EXECUTED", "sms.read")
+ }
+
+ "sms.conversation" -> {
+ val threadId = params.optLong("threadId")
+ val limit = params.optInt("limit", 50)
+ val messages = SmsProvider.getConversation(threadId, limit)
+ val arr = JSONArray()
+ messages.forEach { arr.put(it.toJson()) }
+ sendDataResponse(commandId, JSONObject().put("messages", arr))
+ ClawdNodeApp.instance.auditLog.log("COMMAND_EXECUTED", "sms.conversation")
+ }
+
+ "sms.threads" -> {
+ val limit = params.optInt("limit", 20)
+ val threads = SmsProvider.getThreads(limit)
+ val arr = JSONArray()
+ threads.forEach { arr.put(it.toJson()) }
+ sendDataResponse(commandId, JSONObject().put("threads", arr))
+ ClawdNodeApp.instance.auditLog.log("COMMAND_EXECUTED", "sms.threads")
+ }
+
+ "sms.send" -> {
+ val to = params.optString("to")
+ val body = params.optString("body")
+ if (to.isBlank() || body.isBlank()) {
+ sendResponse(commandId, false, "Missing 'to' or 'body'")
+ } else {
+ SmsProvider.sendSms(to, body)
+ sendResponse(commandId, true)
+ }
+ ClawdNodeApp.instance.auditLog.log("COMMAND_EXECUTED", "sms.send to=$to")
+ }
+
else -> {
Log.w(TAG, "Unknown command: $command")
sendResponse(commandId, false, "Unknown command: $command")
@@ -252,6 +308,20 @@ object DirectGateway {
"error" to error
))
}
+
+ private fun sendDataResponse(commandId: String, data: JSONObject) {
+ if (!isConnected) return
+ try {
+ val json = JSONObject()
+ json.put("type", "response")
+ json.put("commandId", commandId)
+ json.put("success", true)
+ json.put("data", data)
+ webSocket?.send(json.toString())
+ } catch (e: Exception) {
+ Log.e(TAG, "sendDataResponse failed", e)
+ }
+ }
// ========================================
// Outgoing events
@@ -293,6 +363,19 @@ object DirectGateway {
))
}
+ fun sendSmsReceived(id: Long, address: String, contactName: String?, body: String, date: Long, threadId: Long) {
+ send(mapOf(
+ "type" to "sms.received",
+ "id" to id,
+ "address" to address,
+ "contactName" to contactName,
+ "body" to body,
+ "date" to date,
+ "threadId" to threadId,
+ "timestamp" to System.currentTimeMillis()
+ ))
+ }
+
fun sendLog(message: String, data: Map = emptyMap()) {
send(mapOf("type" to "log", "message" to message) + data)
}
diff --git a/app/src/main/java/com/inou/clawdnode/service/NodeService.kt b/app/src/main/java/com/inou/clawdnode/service/NodeService.kt
index a8baa6e..b9d746c 100644
--- a/app/src/main/java/com/inou/clawdnode/service/NodeService.kt
+++ b/app/src/main/java/com/inou/clawdnode/service/NodeService.kt
@@ -12,6 +12,7 @@ import com.inou.clawdnode.ClawdNodeApp
import com.inou.clawdnode.R
import com.inou.clawdnode.gateway.DirectGateway
import com.inou.clawdnode.protocol.*
+import com.inou.clawdnode.sms.SmsMonitor
import com.inou.clawdnode.ui.MainActivity
/**
@@ -24,6 +25,7 @@ class NodeService : Service() {
private val binder = LocalBinder()
private var isConnected = false
+ private val smsMonitor = SmsMonitor()
// Callbacks for UI updates
var onConnectionChange: ((Boolean) -> Unit)? = null
@@ -62,6 +64,9 @@ class NodeService : Service() {
DirectGateway.onCallHangup = { callId ->
CallManager.hangup(callId)
}
+
+ // Start SMS monitor
+ smsMonitor.start()
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
@@ -78,6 +83,7 @@ class NodeService : Service() {
override fun onDestroy() {
Log.i(tag, "NodeService destroyed")
+ smsMonitor.stop()
DirectGateway.disconnect()
ClawdNodeApp.instance.auditLog.log("SERVICE_STOP", "NodeService destroyed")
super.onDestroy()
diff --git a/app/src/main/java/com/inou/clawdnode/sms/SmsMonitor.kt b/app/src/main/java/com/inou/clawdnode/sms/SmsMonitor.kt
new file mode 100644
index 0000000..671c910
--- /dev/null
+++ b/app/src/main/java/com/inou/clawdnode/sms/SmsMonitor.kt
@@ -0,0 +1,99 @@
+package com.inou.clawdnode.sms
+
+import android.database.ContentObserver
+import android.net.Uri
+import android.os.Handler
+import android.os.Looper
+import android.util.Log
+import com.inou.clawdnode.ClawdNodeApp
+import com.inou.clawdnode.debug.DebugClient
+import com.inou.clawdnode.gateway.DirectGateway
+
+/**
+ * Monitors content://sms/ for new incoming messages via ContentObserver.
+ * Reports new SMS to the gateway as "sms.received" events.
+ */
+class SmsMonitor {
+ private val TAG = "SmsMonitor"
+ private var observer: ContentObserver? = null
+ private var lastSeenId: Long = -1
+ private var started = false
+
+ fun start() {
+ if (started) return
+ started = true
+
+ // Initialize lastSeenId to current max
+ try {
+ ClawdNodeApp.instance.contentResolver.query(
+ Uri.parse("content://sms/"),
+ arrayOf("_id"),
+ null, null, "_id DESC LIMIT 1"
+ )?.use { cursor ->
+ if (cursor.moveToFirst()) {
+ lastSeenId = cursor.getLong(0)
+ }
+ }
+ } catch (e: Exception) {
+ Log.w(TAG, "Failed to get initial SMS ID", e)
+ }
+
+ observer = object : ContentObserver(Handler(Looper.getMainLooper())) {
+ override fun onChange(selfChange: Boolean) {
+ checkNewMessages()
+ }
+ }
+
+ ClawdNodeApp.instance.contentResolver.registerContentObserver(
+ Uri.parse("content://sms/"),
+ true,
+ observer!!
+ )
+
+ Log.i(TAG, "SmsMonitor started, lastSeenId=$lastSeenId")
+ DebugClient.log("SmsMonitor started", mapOf("lastSeenId" to lastSeenId))
+ }
+
+ fun stop() {
+ if (!started) return
+ observer?.let {
+ ClawdNodeApp.instance.contentResolver.unregisterContentObserver(it)
+ }
+ observer = null
+ started = false
+ Log.i(TAG, "SmsMonitor stopped")
+ }
+
+ private fun checkNewMessages() {
+ try {
+ ClawdNodeApp.instance.contentResolver.query(
+ Uri.parse("content://sms/inbox"),
+ arrayOf("_id", "address", "body", "date", "thread_id"),
+ if (lastSeenId > 0) "_id > ?" else null,
+ if (lastSeenId > 0) arrayOf(lastSeenId.toString()) else null,
+ "_id ASC"
+ )?.use { cursor ->
+ while (cursor.moveToNext()) {
+ val id = cursor.getLong(0)
+ val address = cursor.getString(1) ?: ""
+ val body = cursor.getString(2) ?: ""
+ val date = cursor.getLong(3)
+ val threadId = cursor.getLong(4)
+ val contactName = SmsProvider.resolveContact(address)
+
+ DirectGateway.sendSmsReceived(id, address, contactName, body, date, threadId)
+ lastSeenId = id
+
+ DebugClient.log("New SMS detected", mapOf(
+ "id" to id,
+ "from" to address,
+ "contact" to (contactName ?: "unknown")
+ ))
+ }
+ }
+ } catch (e: Exception) {
+ Log.e(TAG, "checkNewMessages failed", e)
+ DebugClient.error("SmsMonitor.checkNewMessages failed", e)
+ }
+ }
+}
diff --git a/app/src/main/java/com/inou/clawdnode/ui/MainActivity.kt b/app/src/main/java/com/inou/clawdnode/ui/MainActivity.kt
index bf849c6..db6f07e 100644
--- a/app/src/main/java/com/inou/clawdnode/ui/MainActivity.kt
+++ b/app/src/main/java/com/inou/clawdnode/ui/MainActivity.kt
@@ -275,7 +275,10 @@ class MainActivity : AppCompatActivity() {
Manifest.permission.ANSWER_PHONE_CALLS,
Manifest.permission.RECORD_AUDIO,
Manifest.permission.READ_CONTACTS,
- Manifest.permission.POST_NOTIFICATIONS
+ Manifest.permission.POST_NOTIFICATIONS,
+ Manifest.permission.READ_SMS,
+ Manifest.permission.SEND_SMS,
+ Manifest.permission.RECEIVE_SMS
)
}
}