chore: auto-commit uncommitted changes

This commit is contained in:
James (ClawdBot) 2026-02-08 00:30:06 -05:00
parent ed7e23003c
commit 849ded12b1
1 changed files with 224 additions and 0 deletions

View File

@ -0,0 +1,224 @@
package com.inou.clawdnode.sms
import android.content.ContentResolver
import android.database.Cursor
import android.net.Uri
import android.provider.ContactsContract
import android.telephony.SmsManager
import android.util.Log
import com.inou.clawdnode.ClawdNodeApp
import com.inou.clawdnode.debug.DebugClient
import org.json.JSONArray
import org.json.JSONObject
data class SmsMessage(
val id: Long,
val address: String,
val contactName: String?,
val body: String,
val date: Long,
val read: Boolean,
val type: Int,
val threadId: Long
) {
fun toJson(): JSONObject = JSONObject().apply {
put("id", id)
put("address", address)
put("contactName", contactName ?: JSONObject.NULL)
put("body", body)
put("date", date)
put("read", read)
put("type", type)
put("threadId", threadId)
}
}
data class SmsThread(
val threadId: Long,
val address: String,
val contactName: String?,
val lastMessage: String,
val lastDate: Long,
val messageCount: Int
) {
fun toJson(): JSONObject = JSONObject().apply {
put("threadId", threadId)
put("address", address)
put("contactName", contactName ?: JSONObject.NULL)
put("lastMessage", lastMessage)
put("lastDate", lastDate)
put("messageCount", messageCount)
}
}
object SmsProvider {
private const val TAG = "SmsProvider"
private val contentResolver: ContentResolver
get() = ClawdNodeApp.instance.contentResolver
private val contactCache = mutableMapOf<String, String?>()
fun resolveContact(address: String): String? {
if (address.isBlank()) return null
contactCache[address]?.let { return it }
return try {
val uri = Uri.withAppendedPath(
ContactsContract.PhoneLookup.CONTENT_FILTER_URI,
Uri.encode(address)
)
contentResolver.query(uri, arrayOf(ContactsContract.PhoneLookup.DISPLAY_NAME), null, null, null)?.use { cursor ->
if (cursor.moveToFirst()) {
cursor.getString(0)
} else null
}.also { contactCache[address] = it }
} catch (e: Exception) {
Log.w(TAG, "Contact lookup failed for $address: ${e.message}")
null
}
}
private fun cursorToMessage(cursor: Cursor): SmsMessage {
val address = cursor.getString(cursor.getColumnIndexOrThrow("address")) ?: ""
return SmsMessage(
id = cursor.getLong(cursor.getColumnIndexOrThrow("_id")),
address = address,
contactName = resolveContact(address),
body = cursor.getString(cursor.getColumnIndexOrThrow("body")) ?: "",
date = cursor.getLong(cursor.getColumnIndexOrThrow("date")),
read = cursor.getInt(cursor.getColumnIndexOrThrow("read")) == 1,
type = cursor.getInt(cursor.getColumnIndexOrThrow("type")),
threadId = cursor.getLong(cursor.getColumnIndexOrThrow("thread_id"))
)
}
fun listMessages(limit: Int = 50, offset: Int = 0, since: Long? = null, type: Int? = null): List<SmsMessage> {
val selection = mutableListOf<String>()
val args = mutableListOf<String>()
since?.let {
selection.add("date > ?")
args.add(it.toString())
}
type?.let {
selection.add("type = ?")
args.add(it.toString())
}
val where = if (selection.isEmpty()) null else selection.joinToString(" AND ")
val messages = mutableListOf<SmsMessage>()
try {
contentResolver.query(
Uri.parse("content://sms/"),
arrayOf("_id", "address", "body", "date", "read", "type", "thread_id"),
where,
if (args.isEmpty()) null else args.toTypedArray(),
"date DESC LIMIT $limit OFFSET $offset"
)?.use { cursor ->
while (cursor.moveToNext()) {
messages.add(cursorToMessage(cursor))
}
}
} catch (e: Exception) {
Log.e(TAG, "listMessages failed", e)
DebugClient.error("SmsProvider.listMessages failed", e)
}
return messages
}
fun getMessage(id: Long): SmsMessage? {
return try {
contentResolver.query(
Uri.parse("content://sms/$id"),
arrayOf("_id", "address", "body", "date", "read", "type", "thread_id"),
null, null, null
)?.use { cursor ->
if (cursor.moveToFirst()) cursorToMessage(cursor) else null
}
} catch (e: Exception) {
Log.e(TAG, "getMessage failed for id=$id", e)
null
}
}
fun getConversation(threadId: Long, limit: Int = 50): List<SmsMessage> {
val messages = mutableListOf<SmsMessage>()
try {
contentResolver.query(
Uri.parse("content://sms/"),
arrayOf("_id", "address", "body", "date", "read", "type", "thread_id"),
"thread_id = ?",
arrayOf(threadId.toString()),
"date DESC LIMIT $limit"
)?.use { cursor ->
while (cursor.moveToNext()) {
messages.add(cursorToMessage(cursor))
}
}
} catch (e: Exception) {
Log.e(TAG, "getConversation failed for thread=$threadId", e)
}
return messages
}
fun getThreads(limit: Int = 20): List<SmsThread> {
val threads = mutableListOf<SmsThread>()
try {
// Get distinct thread IDs with latest message
contentResolver.query(
Uri.parse("content://sms/"),
arrayOf("_id", "address", "body", "date", "read", "type", "thread_id"),
null, null, "date DESC"
)?.use { cursor ->
val seenThreads = mutableSetOf<Long>()
val threadCounts = mutableMapOf<Long, Int>()
// First pass: count messages per thread
while (cursor.moveToNext()) {
val tid = cursor.getLong(cursor.getColumnIndexOrThrow("thread_id"))
threadCounts[tid] = (threadCounts[tid] ?: 0) + 1
}
// Second pass: get latest message per thread
cursor.moveToPosition(-1)
while (cursor.moveToNext() && seenThreads.size < limit) {
val tid = cursor.getLong(cursor.getColumnIndexOrThrow("thread_id"))
if (seenThreads.contains(tid)) continue
seenThreads.add(tid)
val address = cursor.getString(cursor.getColumnIndexOrThrow("address")) ?: ""
threads.add(SmsThread(
threadId = tid,
address = address,
contactName = resolveContact(address),
lastMessage = cursor.getString(cursor.getColumnIndexOrThrow("body")) ?: "",
lastDate = cursor.getLong(cursor.getColumnIndexOrThrow("date")),
messageCount = threadCounts[tid] ?: 0
))
}
}
} catch (e: Exception) {
Log.e(TAG, "getThreads failed", e)
}
return threads
}
fun sendSms(to: String, body: String) {
try {
val smsManager = SmsManager.getDefault()
val parts = smsManager.divideMessage(body)
if (parts.size == 1) {
smsManager.sendTextMessage(to, null, body, null, null)
} else {
smsManager.sendMultipartTextMessage(to, null, parts, null, null)
}
DebugClient.log("SMS sent", mapOf("to" to to, "length" to body.length))
} catch (e: Exception) {
Log.e(TAG, "sendSms failed", e)
DebugClient.error("SmsProvider.sendSms failed", e)
throw e
}
}
}