package com.simplemobiletools.smsmessenger.helpers import android.annotation.SuppressLint import android.content.Context import android.net.Uri import android.provider.Telephony.* import android.util.Base64 import android.util.Log import com.simplemobiletools.commons.extensions.* import com.simplemobiletools.commons.helpers.isQPlus import com.simplemobiletools.smsmessenger.extensions.rowsToJson import com.simplemobiletools.smsmessenger.models.MmsAddress import com.simplemobiletools.smsmessenger.models.MmsBackup import com.simplemobiletools.smsmessenger.models.MmsPart import com.simplemobiletools.smsmessenger.models.SmsBackup import java.io.IOException import java.io.InputStream class MessagesReader(private val context: Context) { companion object { private const val TAG = "MessagesReader" } fun forEachSms(threadId: Long, block: (SmsBackup) -> Unit) { val projection = arrayOf( Sms.SUBSCRIPTION_ID, Sms.ADDRESS, Sms.BODY, Sms.DATE, Sms.DATE_SENT, Sms.LOCKED, Sms.PROTOCOL, Sms.READ, Sms.STATUS, Sms.TYPE, Sms.SERVICE_CENTER ) val selection = "${Sms.THREAD_ID} = ?" val selectionArgs = arrayOf(threadId.toString()) context.queryCursor(Sms.CONTENT_URI, projection, selection, selectionArgs) { cursor -> val subscriptionId = cursor.getLongValue(Sms.SUBSCRIPTION_ID) val address = cursor.getStringValue(Sms.ADDRESS) val body = cursor.getStringValueOrNull(Sms.BODY) val date = cursor.getLongValue(Sms.DATE) val dateSent = cursor.getLongValue(Sms.DATE_SENT) val locked = cursor.getIntValue(Sms.DATE_SENT) val protocol = cursor.getStringValueOrNull(Sms.PROTOCOL) val read = cursor.getIntValue(Sms.READ) val status = cursor.getIntValue(Sms.STATUS) val type = cursor.getIntValue(Sms.TYPE) val serviceCenter = cursor.getStringValueOrNull(Sms.SERVICE_CENTER) block(SmsBackup(subscriptionId, address, body, date, dateSent, locked, protocol, read, status, type, serviceCenter)) } } // all mms from simple sms are non-text messages fun forEachMms(threadId: Long, includeTextOnlyAttachment: Boolean = false, block: (MmsBackup) -> Unit) { val projection = arrayOf( Mms._ID, Mms.CREATOR, Mms.CONTENT_TYPE, Mms.DELIVERY_REPORT, Mms.DATE, Mms.DATE_SENT, Mms.LOCKED, Mms.MESSAGE_TYPE, Mms.MESSAGE_BOX, Mms.READ, Mms.READ_REPORT, Mms.SEEN, Mms.TEXT_ONLY, Mms.STATUS, Mms.SUBJECT_CHARSET, Mms.SUBSCRIPTION_ID, Mms.TRANSACTION_ID ) val selection = if (includeTextOnlyAttachment) { "${Mms.THREAD_ID} = ? AND ${Mms.TEXT_ONLY} = ?" } else { "${Mms.THREAD_ID} = ?" } val selectionArgs = if (includeTextOnlyAttachment) { arrayOf(threadId.toString(), "1") } else { arrayOf(threadId.toString()) } context.queryCursor(Mms.CONTENT_URI, projection, selection, selectionArgs) { cursor -> val mmsId = cursor.getLongValue(Mms._ID) val creator = cursor.getStringValueOrNull(Mms.CREATOR) val contentType = cursor.getStringValueOrNull(Mms.CONTENT_TYPE) val deliveryReport = cursor.getIntValue(Mms.DELIVERY_REPORT) val date = cursor.getLongValue(Mms.DATE) val dateSent = cursor.getLongValue(Mms.DATE_SENT) val locked = cursor.getIntValue(Mms.LOCKED) val messageType = cursor.getIntValue(Mms.MESSAGE_TYPE) val messageBox = cursor.getIntValue(Mms.MESSAGE_BOX) val read = cursor.getIntValue(Mms.READ) val readReport = cursor.getIntValue(Mms.READ_REPORT) val seen = cursor.getIntValue(Mms.SEEN) val textOnly = cursor.getIntValue(Mms.TEXT_ONLY) val status = cursor.getStringValueOrNull(Mms.STATUS) val subject = cursor.getStringValueOrNull(Mms.SUBJECT) val subjectCharSet = cursor.getStringValueOrNull(Mms.SUBJECT_CHARSET) val subscriptionId = cursor.getLongValue(Mms.SUBSCRIPTION_ID) val transactionId = cursor.getStringValueOrNull(Mms.TRANSACTION_ID) val parts = getParts(mmsId) val addresses = getMMSAddresses(mmsId) block(MmsBackup(creator, contentType, deliveryReport, date, dateSent, locked, messageType, messageBox, read, readReport, seen, textOnly, status, subject, subjectCharSet, subscriptionId, transactionId, addresses, parts)) } } @SuppressLint("NewApi") private fun getParts(mmsId: Long): List { val parts = mutableListOf() val uri = if (isQPlus()) { Mms.Part.CONTENT_URI } else { Uri.parse("content://mms/part") } val projection = arrayOf( Mms.Part._ID, Mms.Part.CONTENT_DISPOSITION, Mms.Part.CHARSET, Mms.Part.CONTENT_ID, Mms.Part.CONTENT_LOCATION, Mms.Part.CONTENT_TYPE, Mms.Part.CT_START, Mms.Part.CT_TYPE, Mms.Part.FILENAME, Mms.Part.NAME, Mms.Part.SEQ, Mms.Part.TEXT ) val selection = "${Mms.Part.MSG_ID}= ?" val selectionArgs = arrayOf(mmsId.toString()) context.queryCursor(uri, projection, selection, selectionArgs) { cursor -> val partId = cursor.getLongValue(Mms.Part._ID) val contentDisposition = cursor.getStringValueOrNull(Mms.Part.CONTENT_DISPOSITION) val charset = cursor.getStringValueOrNull(Mms.Part.CHARSET) val contentId = cursor.getStringValueOrNull(Mms.Part.CONTENT_ID) val contentLocation = cursor.getStringValueOrNull(Mms.Part.CONTENT_LOCATION) val contentType = cursor.getStringValue(Mms.Part.CONTENT_TYPE) val ctStart = cursor.getStringValueOrNull(Mms.Part.CT_START) val ctType = cursor.getStringValueOrNull(Mms.Part.CT_TYPE) val filename = cursor.getStringValueOrNull(Mms.Part.FILENAME) val name = cursor.getStringValueOrNull(Mms.Part.NAME) val sequenceOrder = cursor.getIntValue(Mms.Part.SEQ) val text = cursor.getStringValueOrNull(Mms.Part.TEXT) val data = when { contentType.startsWith("text/") -> { usePart(partId) { stream -> stream.readBytes().toString(Charsets.UTF_8) } } else -> { usePart(partId) { stream -> val arr = stream.readBytes() Log.d(TAG, "getParts: $arr") Log.d(TAG, "getParts: size = ${arr.size}") Log.d(TAG, "getParts: US_ASCII-> ${arr.toString(Charsets.US_ASCII)}") Log.d(TAG, "getParts: UTF_8-> ${arr.toString(Charsets.UTF_8)}") Base64.encodeToString(arr, Base64.DEFAULT) } } } parts.add(MmsPart(contentDisposition, charset, contentId, contentLocation, contentType, ctStart, ctType, filename, name, sequenceOrder, text, data)) } return parts } @SuppressLint("NewApi") private fun usePart(partId: Long, block: (InputStream) -> String): String { val partUri = if (isQPlus()) { Mms.Part.CONTENT_URI.buildUpon().appendPath(partId.toString()).build() } else { Uri.parse("content://mms/part/$partId") } try { val stream = context.contentResolver.openInputStream(partUri) if (stream == null) { val msg = "failed opening stream for mms part $partUri" Log.e(TAG, msg) return "" } stream.use { return block(stream) } } catch (e: IOException) { val msg = "failed to read MMS part on $partUri" Log.e(TAG, msg, e) return "" } } @SuppressLint("NewApi") private fun getMMSAddresses(messageId: Long): List { val addresses = mutableListOf() val uri = if (isQPlus()) { Mms.Addr.getAddrUriForMessage(messageId.toString()) } else { Uri.parse("content://mms/$messageId/addr") } val projection = arrayOf(Mms.Addr.ADDRESS, Mms.Addr.TYPE, Mms.Addr.CHARSET) val selection = "${Mms.Addr.MSG_ID}= ?" val selectionArgs = arrayOf(messageId.toString()) context.queryCursor(uri, projection, selection, selectionArgs) { cursor -> val address = cursor.getStringValue(Mms.Addr.ADDRESS) val type = cursor.getIntValue(Mms.Addr.TYPE) val charset = cursor.getIntValue(Mms.Addr.CHARSET) addresses.add(MmsAddress(address, type, charset)) } return addresses } fun getMessagesCount(): Long { return 0 } }