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