From 5e04fc3fe07eb902d0b3394b02dd2866b3adc23f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leonard=20Janis=20Robert=20K=C3=B6nig?= Date: Fri, 23 Jan 2026 00:34:11 +0100 Subject: [PATCH 1/5] fix: Add missing column to projection Otherwise the queryCursor will fail silently (since showErrors = false) with the exception column 'sub' does not exist. Available columns: [_id, creator, ct_t, d_rpt, date, date_sent, locked, m_type, msg_box, read, rr, seen, text_only, st, sub_cs, sub_id, tr_id] and will return no results at all. --- .../main/kotlin/org/fossify/messages/helpers/MessagesReader.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/main/kotlin/org/fossify/messages/helpers/MessagesReader.kt b/app/src/main/kotlin/org/fossify/messages/helpers/MessagesReader.kt index cf8bd424b..939b70d0e 100644 --- a/app/src/main/kotlin/org/fossify/messages/helpers/MessagesReader.kt +++ b/app/src/main/kotlin/org/fossify/messages/helpers/MessagesReader.kt @@ -115,6 +115,7 @@ class MessagesReader(private val context: Context) { Mms.SEEN, Mms.TEXT_ONLY, Mms.STATUS, + Mms.SUBJECT, Mms.SUBJECT_CHARSET, Mms.SUBSCRIPTION_ID, Mms.TRANSACTION_ID From 63bab12d562eca2d55f68fcb96bee72faef3f175 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leonard=20Janis=20Robert=20K=C3=B6nig?= Date: Fri, 23 Jan 2026 00:40:09 +0100 Subject: [PATCH 2/5] fix: SMS "locked" wrong value in export --- .../main/kotlin/org/fossify/messages/helpers/MessagesReader.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/kotlin/org/fossify/messages/helpers/MessagesReader.kt b/app/src/main/kotlin/org/fossify/messages/helpers/MessagesReader.kt index 939b70d0e..57e4d7f97 100644 --- a/app/src/main/kotlin/org/fossify/messages/helpers/MessagesReader.kt +++ b/app/src/main/kotlin/org/fossify/messages/helpers/MessagesReader.kt @@ -70,7 +70,7 @@ class MessagesReader(private val context: Context) { 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 locked = cursor.getIntValue(Sms.LOCKED) val protocol = cursor.getStringValueOrNull(Sms.PROTOCOL) val read = cursor.getIntValue(Sms.READ) val status = cursor.getIntValue(Sms.STATUS) From 0f0f97aa8f47e9f04b3402f7e4d34f45e4700c76 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leonard=20Janis=20Robert=20K=C3=B6nig?= Date: Fri, 23 Jan 2026 00:47:41 +0100 Subject: [PATCH 3/5] Implement top-level logic for text-only MMS exports Currently, the flag `includeTextOnlyAttachment` is never used in the export logic, despite being of useful value: Some MMS aren't immediately visible to users as such (e.g., very long "SMS" messages are often sent as MMS). Users wanting to only export "true" MMS (images, etc.), may be confused as to why "SMS" are included in the export. As such, introduce a logic to set this flag if only MMS export is checked, otherwise export them as-is. Similarly, users exporting SMS only may be missing these exact messages. Analogously, introduce a new flag `includeTextOnlyMMSasSMS` for SMS export, that is suppoed to export long MMS "as SMS". --- .../org/fossify/messages/helpers/MessagesReader.kt | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/app/src/main/kotlin/org/fossify/messages/helpers/MessagesReader.kt b/app/src/main/kotlin/org/fossify/messages/helpers/MessagesReader.kt index 57e4d7f97..3e1ee7523 100644 --- a/app/src/main/kotlin/org/fossify/messages/helpers/MessagesReader.kt +++ b/app/src/main/kotlin/org/fossify/messages/helpers/MessagesReader.kt @@ -3,10 +3,14 @@ package org.fossify.messages.helpers import android.annotation.SuppressLint import android.content.Context import android.net.Uri +import android.provider.Telephony import android.provider.Telephony.Mms import android.provider.Telephony.Sms import android.util.Base64 +import com.google.android.mms.ContentType +import com.google.android.mms.pdu_alt.PduHeaders import org.fossify.commons.extensions.getIntValue +import org.fossify.commons.extensions.getIntValueOrNull import org.fossify.commons.extensions.getLongValue import org.fossify.commons.extensions.getStringValue import org.fossify.commons.extensions.getStringValueOrNull @@ -31,16 +35,20 @@ class MessagesReader(private val context: Context) { var smsMessages = listOf() var mmsMessages = listOf() + // Special-case "Text only" MMS: + // - If only-backup-sms: backup text-only MMS as SMS + // - If only-backup-mms: exclude text-only MMS + // - If both: backup text-only MMS as MMS if (getSms) { - smsMessages = getSmsMessages(conversationIds) + smsMessages = getSmsMessages(conversationIds, includeTextOnlyMMSasSMS = !getMms) } if (getMms) { - mmsMessages = getMmsMessages(conversationIds) + mmsMessages = getMmsMessages(conversationIds, getSms) } callback(smsMessages + mmsMessages) } - private fun getSmsMessages(threadIds: List): List { + private fun getSmsMessages(threadIds: List, includeTextOnlyMMSasSMS: Boolean = false): List { val projection = arrayOf( Sms.SUBSCRIPTION_ID, Sms.ADDRESS, From e464423de778e7932e2f56b7bd2d7fa7d96d0611 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leonard=20Janis=20Robert=20K=C3=B6nig?= Date: Fri, 23 Jan 2026 00:52:57 +0100 Subject: [PATCH 4/5] Implement `includeTextOnlyMMSasSMS` This effectively converts MMS that are "text only" to SMS as part of the export process. Property values from MMS are converted to equivalents in SMS. --- .../messages/helpers/MessagesReader.kt | 111 ++++++++++++++++++ 1 file changed, 111 insertions(+) diff --git a/app/src/main/kotlin/org/fossify/messages/helpers/MessagesReader.kt b/app/src/main/kotlin/org/fossify/messages/helpers/MessagesReader.kt index 3e1ee7523..acb5a0605 100644 --- a/app/src/main/kotlin/org/fossify/messages/helpers/MessagesReader.kt +++ b/app/src/main/kotlin/org/fossify/messages/helpers/MessagesReader.kt @@ -101,6 +101,117 @@ class MessagesReader(private val context: Context) { ) } } + + /* stores text only MMS as SMS */ + if (!includeTextOnlyMMSasSMS) { return smsList } + val mmsProjection = arrayOf( + Mms._ID, + + Mms.SUBSCRIPTION_ID, + /* ADDRESS : getMmsAddresses() */ + /* BODY : getParts() */ + Mms.DATE, + Mms.DATE_SENT, + Mms.LOCKED, + /* PROTOCOL : null */ + Mms.READ, + Mms.STATUS, + Mms.MESSAGE_BOX, + /* SERVICE_CENTER : null */ + ) + + val mmsSelection = "${Mms.THREAD_ID} = ?" + + threadIds.map { it.toString() }.forEach { threadId -> + val selectionArgs = arrayOf(threadId) + context.queryCursor(Mms.CONTENT_URI, mmsProjection, mmsSelection, selectionArgs) { cursor -> + val mmsId = cursor.getLongValue(Mms._ID) + + val subscriptionId = cursor.getLongValue(Mms.SUBSCRIPTION_ID) + val addresses = getMmsAddresses(mmsId) + val address = addresses.first { it.type == PduHeaders.FROM }.address + + /* body done at the end */ + + val date = cursor.getLongValue(Mms.DATE) * 1000 + val dateSent = cursor.getLongValue(Mms.DATE_SENT) * 1000 + val locked = cursor.getIntValue(Mms.LOCKED) + val protocol = null + val read = cursor.getIntValue(Mms.READ) + val mmsStatus = cursor.getIntValueOrNull(Mms.STATUS) + val status = when (mmsStatus) { + null, PduHeaders.STATUS_UNRECOGNIZED, PduHeaders.STATUS_INDETERMINATE -> { + Telephony.TextBasedSmsColumns.STATUS_NONE + } + PduHeaders.STATUS_EXPIRED, PduHeaders.STATUS_UNREACHABLE, PduHeaders.STATUS_REJECTED -> { + Telephony.TextBasedSmsColumns.STATUS_FAILED + } + PduHeaders.STATUS_DEFERRED, PduHeaders.STATUS_FORWARDED -> { + Telephony.TextBasedSmsColumns.STATUS_PENDING + } + PduHeaders.STATUS_RETRIEVED -> { + Telephony.TextBasedSmsColumns.STATUS_COMPLETE + } + else -> { + /* unreachable? */ + Telephony.TextBasedSmsColumns.STATUS_NONE + } + } + + val messageBox = cursor.getIntValue(Mms.MESSAGE_BOX) + val type = when (messageBox) { + Telephony.BaseMmsColumns.MESSAGE_BOX_INBOX -> { + Telephony.TextBasedSmsColumns.MESSAGE_TYPE_INBOX + } + Telephony.BaseMmsColumns.MESSAGE_BOX_OUTBOX -> { + Telephony.TextBasedSmsColumns.MESSAGE_TYPE_OUTBOX + } + Telephony.BaseMmsColumns.MESSAGE_BOX_DRAFTS -> { + Telephony.TextBasedSmsColumns.MESSAGE_TYPE_DRAFT + } + Telephony.BaseMmsColumns.MESSAGE_BOX_FAILED -> { + Telephony.TextBasedSmsColumns.MESSAGE_TYPE_FAILED + } + Telephony.BaseMmsColumns.MESSAGE_BOX_SENT -> { + Telephony.TextBasedSmsColumns.MESSAGE_TYPE_SENT + } + else -> { + /* unreachable */ + Telephony.TextBasedSmsColumns.MESSAGE_TYPE_ALL + } + } + + val serviceCenter = null + + /* We do not rely on TEXT_ONLY MMS flag, see also getMmsMessages() */ + val parts = getParts(mmsId) + val smil = parts.filter { it.contentType == ContentType.APP_SMIL } + val plain = parts.filter { it.contentType == ContentType.TEXT_PLAIN } + val others = parts.filter { it.contentType != ContentType.APP_SMIL && it.contentType != ContentType.TEXT_PLAIN } + + if (smil.size <= 1 && plain.size == 1 && others.size == 0) { + val body = plain.first().text + + smsList.add( + SmsBackup( + subscriptionId = subscriptionId, + address = address, + body = body, + date = date, + dateSent = dateSent, + locked = locked, + protocol = protocol, + read = read, + status = status, + type = type, + serviceCenter = serviceCenter + ) + ) + } + } + } + + return smsList } From 4351c891efc83b8a0668b1263df0ce402220a153 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leonard=20Janis=20Robert=20K=C3=B6nig?= Date: Fri, 23 Jan 2026 00:54:14 +0100 Subject: [PATCH 5/5] Rework/fix includeTextOnlyAttachment This flag behaved oddly, as text only attachments were *always* included, even if it is not set. However, setting it, would filter the MMS results for those MMS exclusively, that had TEXT_ONLY set. This old behavior would've been better described by "onlyIncludeTextOnlyAttachments". In addition, however, the logic is flawed as TEXT_ONLY is a property set by the PduPersister when receiving MMS. If the MMS wasn't processed by the Persister, the default value of the table row, 0, is set. This is quite often the case (in my case, all MMS were thus set to be non-text-only, despite being very much text-only). The AOSP source code also mentions that this is only a UI hint. For exports, we thus re-implement the same logic and check whether MMS messages are indeed "text only" or not. --- .../messages/helpers/MessagesReader.kt | 80 +++++++++++-------- 1 file changed, 47 insertions(+), 33 deletions(-) diff --git a/app/src/main/kotlin/org/fossify/messages/helpers/MessagesReader.kt b/app/src/main/kotlin/org/fossify/messages/helpers/MessagesReader.kt index acb5a0605..a8f8bcaa7 100644 --- a/app/src/main/kotlin/org/fossify/messages/helpers/MessagesReader.kt +++ b/app/src/main/kotlin/org/fossify/messages/helpers/MessagesReader.kt @@ -239,19 +239,11 @@ class MessagesReader(private val context: Context) { Mms.SUBSCRIPTION_ID, Mms.TRANSACTION_ID ) - val selection = if (includeTextOnlyAttachment) { - "${Mms.THREAD_ID} = ? AND ${Mms.TEXT_ONLY} = ?" - } else { - "${Mms.THREAD_ID} = ?" - } + val selection = "${Mms.THREAD_ID} = ?" val mmsList = mutableListOf() threadIds.map { it.toString() }.forEach { threadId -> - val selectionArgs = if (includeTextOnlyAttachment) { - arrayOf(threadId, "1") - } else { - arrayOf(threadId) - } + val selectionArgs = arrayOf(threadId) context.queryCursor(Mms.CONTENT_URI, projection, selection, selectionArgs) { cursor -> val mmsId = cursor.getLongValue(Mms._ID) val creator = cursor.getStringValueOrNull(Mms.CREATOR) @@ -265,7 +257,7 @@ class MessagesReader(private val context: Context) { 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) + var 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) @@ -274,29 +266,51 @@ class MessagesReader(private val context: Context) { val parts = getParts(mmsId) val addresses = getMmsAddresses(mmsId) - mmsList.add( - MmsBackup( - creator = creator, - contentType = contentType, - deliveryReport = deliveryReport, - date = date, - dateSent = dateSent, - locked = locked, - messageType = messageType, - messageBox = messageBox, - read = read, - readReport = readReport, - seen = seen, - textOnly = textOnly, - status = status, - subject = subject, - subjectCharSet = subjectCharSet, - subscriptionId = subscriptionId, - transactionId = transactionId, - addresses = addresses, - parts = parts + + // If textOnly was set to 1, we trust that judgement, as this was explicitly set. + // However, since 0 is the default value [1], it's common for "text only" messages to have + // this set to 0, despite. This is also the reason why we do not use this flag for filtering + // within the query. + // + // [1]: https://cs.android.com/android/platform/superproject/main/+/main:packages/providers/TelephonyProvider/src/com/android/providers/telephony/MmsSmsDatabaseHelper.java;l=2512;drc=61197364367c9e404c7da6900658f1b16c42d0da + if (textOnly == 0) { + // Effectively the same logic as in AOSP [2] when categorizing incoming messages. + // + // [2]: https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/telephony/common/com/google/android/mms/pdu/PduPersister.java;l=1346;drc=6a5fcafd33cb3068235f1d76a8a6939886e15a71 + textOnly = 1 + if (parts.size > 2) { textOnly = 0 } + for (part in parts) { + if (ContentType.APP_SMIL != part.contentType && ContentType.TEXT_PLAIN != part.contentType) { + textOnly = 0 + } + } + } + + if (includeTextOnlyAttachment || textOnly == 0) { + mmsList.add( + MmsBackup( + creator = creator, + contentType = contentType, + deliveryReport = deliveryReport, + date = date, + dateSent = dateSent, + locked = locked, + messageType = messageType, + messageBox = messageBox, + read = read, + readReport = readReport, + seen = seen, + textOnly = textOnly, + status = status, + subject = subject, + subjectCharSet = subjectCharSet, + subscriptionId = subscriptionId, + transactionId = transactionId, + addresses = addresses, + parts = parts + ) ) - ) + } } } return mmsList