chore: auto-commit uncommitted changes
This commit is contained in:
parent
ed7e23003c
commit
849ded12b1
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue