Improve third party SMS/MMS intent parsing

Closes https://github.com/FossifyOrg/Messages/issues/243

Closes https://github.com/FossifyOrg/Messages/issues/217
This commit is contained in:
Naveen Singh 2025-01-04 03:14:50 +05:30
parent f66aa77831
commit 4434d187bc
No known key found for this signature in database
GPG key ID: AF5D43C216778C0B
3 changed files with 152 additions and 17 deletions

View file

@ -80,7 +80,6 @@
android:parentActivityName=".activities.MainActivity">
<intent-filter>
<action android:name="android.intent.action.SEND" />
<action android:name="android.intent.action.SENDTO" />
<action android:name="android.intent.action.VIEW" />

View file

@ -8,8 +8,31 @@ import android.widget.Toast
import com.google.gson.Gson
import com.reddit.indicatorfastscroll.FastScrollItemIndicator
import org.fossify.commons.dialogs.RadioGroupDialog
import org.fossify.commons.extensions.*
import org.fossify.commons.helpers.*
import org.fossify.commons.extensions.applyColorFilter
import org.fossify.commons.extensions.areSystemAnimationsEnabled
import org.fossify.commons.extensions.beGone
import org.fossify.commons.extensions.beVisible
import org.fossify.commons.extensions.beVisibleIf
import org.fossify.commons.extensions.getColorStateList
import org.fossify.commons.extensions.getContrastColor
import org.fossify.commons.extensions.getMyContactsCursor
import org.fossify.commons.extensions.getPhoneNumberTypeText
import org.fossify.commons.extensions.getProperPrimaryColor
import org.fossify.commons.extensions.getProperTextColor
import org.fossify.commons.extensions.hasPermission
import org.fossify.commons.extensions.hideKeyboard
import org.fossify.commons.extensions.normalizeString
import org.fossify.commons.extensions.onTextChangeListener
import org.fossify.commons.extensions.toast
import org.fossify.commons.extensions.underlineText
import org.fossify.commons.extensions.updateTextColors
import org.fossify.commons.extensions.value
import org.fossify.commons.extensions.viewBinding
import org.fossify.commons.helpers.MyContactsContentProvider
import org.fossify.commons.helpers.NavigationIcon
import org.fossify.commons.helpers.PERMISSION_READ_CONTACTS
import org.fossify.commons.helpers.SimpleContactsHelper
import org.fossify.commons.helpers.ensureBackgroundThread
import org.fossify.commons.models.RadioItem
import org.fossify.commons.models.SimpleContact
import org.fossify.messages.R
@ -18,7 +41,13 @@ import org.fossify.messages.databinding.ActivityNewConversationBinding
import org.fossify.messages.databinding.ItemSuggestedContactBinding
import org.fossify.messages.extensions.getSuggestedContacts
import org.fossify.messages.extensions.getThreadId
import org.fossify.messages.helpers.*
import org.fossify.messages.helpers.SmsIntentParser
import org.fossify.messages.helpers.THREAD_ATTACHMENT_URI
import org.fossify.messages.helpers.THREAD_ATTACHMENT_URIS
import org.fossify.messages.helpers.THREAD_ID
import org.fossify.messages.helpers.THREAD_NUMBER
import org.fossify.messages.helpers.THREAD_TEXT
import org.fossify.messages.helpers.THREAD_TITLE
import org.fossify.messages.messaging.isShortCodeWithLetters
import java.net.URLDecoder
import java.util.Locale
@ -42,7 +71,10 @@ class NewConversationActivity : SimpleActivity() {
useTransparentNavigation = true,
useTopSearchMenu = false
)
setupMaterialScrollListener(scrollingView = binding.contactsList, toolbar = binding.newConversationToolbar)
setupMaterialScrollListener(
scrollingView = binding.contactsList,
toolbar = binding.newConversationToolbar
)
window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE)
binding.newConversationAddress.requestFocus()
@ -112,9 +144,14 @@ class NewConversationActivity : SimpleActivity() {
}
private fun isThirdPartyIntent(): Boolean {
if ((intent.action == Intent.ACTION_SENDTO || intent.action == Intent.ACTION_SEND || intent.action == Intent.ACTION_VIEW) && intent.dataString != null) {
val number = intent.dataString!!.removePrefix("sms:").removePrefix("smsto:").removePrefix("mms").removePrefix("mmsto:").replace("+", "%2b").trim()
launchThreadActivity(URLDecoder.decode(number), "")
val result = SmsIntentParser.parse(intent)
if (result != null) {
val (body, recipients) = result
launchThreadActivity(
phoneNumber = URLDecoder.decode(recipients),
name = "",
body = body
)
finish()
return true
}
@ -142,7 +179,11 @@ class NewConversationActivity : SimpleActivity() {
val hasContacts = contacts.isNotEmpty()
binding.contactsList.beVisibleIf(hasContacts)
binding.noContactsPlaceholder.beVisibleIf(!hasContacts)
binding.noContactsPlaceholder2.beVisibleIf(!hasContacts && !hasPermission(PERMISSION_READ_CONTACTS))
binding.noContactsPlaceholder2.beVisibleIf(
!hasContacts && !hasPermission(
PERMISSION_READ_CONTACTS
)
)
if (!hasContacts) {
val placeholderText = if (hasPermission(PERMISSION_READ_CONTACTS)) {
@ -168,7 +209,13 @@ class NewConversationActivity : SimpleActivity() {
val items = ArrayList<RadioItem>()
phoneNumbers.forEachIndexed { index, phoneNumber ->
val type = getPhoneNumberTypeText(phoneNumber.type, phoneNumber.label)
items.add(RadioItem(index, "${phoneNumber.normalizedNumber} ($type)", phoneNumber.normalizedNumber))
items.add(
RadioItem(
index,
"${phoneNumber.normalizedNumber} ($type)",
phoneNumber.normalizedNumber
)
)
}
RadioGroupDialog(this, items) {
@ -212,10 +259,17 @@ class NewConversationActivity : SimpleActivity() {
suggestedContactName.setTextColor(getProperTextColor())
if (!isDestroyed) {
SimpleContactsHelper(this@NewConversationActivity).loadContactImage(contact.photoUri, suggestedContactImage, contact.name)
SimpleContactsHelper(this@NewConversationActivity).loadContactImage(
contact.photoUri,
suggestedContactImage,
contact.name
)
binding.suggestionsHolder.addView(root)
root.setOnClickListener {
launchThreadActivity(contact.phoneNumbers.first().normalizedNumber, contact.name)
launchThreadActivity(
contact.phoneNumbers.first().normalizedNumber,
contact.name
)
}
}
}
@ -231,28 +285,32 @@ class NewConversationActivity : SimpleActivity() {
try {
val name = contacts[position].name
val character = if (name.isNotEmpty()) name.substring(0, 1) else ""
FastScrollItemIndicator.Text(character.uppercase(Locale.getDefault()).normalizeString())
FastScrollItemIndicator.Text(
character.uppercase(Locale.getDefault()).normalizeString()
)
} catch (e: Exception) {
FastScrollItemIndicator.Text("")
}
})
}
private fun launchThreadActivity(phoneNumber: String, name: String) {
private fun launchThreadActivity(phoneNumber: String, name: String, body: String = "") {
hideKeyboard()
val text = intent.getStringExtra(Intent.EXTRA_TEXT) ?: intent.getStringExtra("sms_body") ?: ""
val numbers = phoneNumber.split(";").toSet()
val number = if (numbers.size == 1) phoneNumber else Gson().toJson(numbers)
Intent(this, ThreadActivity::class.java).apply {
putExtra(THREAD_ID, getThreadId(numbers))
putExtra(THREAD_TITLE, name)
putExtra(THREAD_TEXT, text)
putExtra(THREAD_TEXT, body)
putExtra(THREAD_NUMBER, number)
if (intent.action == Intent.ACTION_SEND && intent.extras?.containsKey(Intent.EXTRA_STREAM) == true) {
val uri = intent.getParcelableExtra<Uri>(Intent.EXTRA_STREAM)
putExtra(THREAD_ATTACHMENT_URI, uri?.toString())
} else if (intent.action == Intent.ACTION_SEND_MULTIPLE && intent.extras?.containsKey(Intent.EXTRA_STREAM) == true) {
} else if (intent.action == Intent.ACTION_SEND_MULTIPLE && intent.extras?.containsKey(
Intent.EXTRA_STREAM
) == true
) {
val uris = intent.getParcelableArrayListExtra<Uri>(Intent.EXTRA_STREAM)
putExtra(THREAD_ATTACHMENT_URIS, uris)
}

View file

@ -0,0 +1,78 @@
package org.fossify.messages.helpers
import android.content.Intent
import android.net.Uri
import com.google.android.mms.ContentType
import java.io.UnsupportedEncodingException
import java.net.URLDecoder
// Base on https://cs.android.com/android/platform/superproject/main/+/main:packages/apps/Messaging/src/com/android/messaging/ui/conversation/LaunchConversationActivity.java
object SmsIntentParser {
private const val SCHEME_SMS = "sms"
private const val SCHEME_SMSTO = "smsto"
private const val SCHEME_MMS = "mms"
private const val SCHEME_MMSTO = "mmsto"
private val SMS_MMS_SCHEMES = setOf(SCHEME_SMS, SCHEME_SMSTO, SCHEME_MMS, SCHEME_MMSTO)
private const val MAX_RECIPIENT_LENGTH = 100
private const val SMS_BODY = "sms_body"
private const val ADDRESS = "address"
fun parse(intent: Intent): Pair<String, String>? {
val action = intent.action
if (action != Intent.ACTION_SENDTO && action != Intent.ACTION_VIEW) {
// Unsupported intent action
return null
}
val recipients = parseRecipients(intent)
val body = extractBodyFromIntent(intent)
return body.orEmpty() to recipients
}
private fun parseRecipients(intent: Intent): String {
val uriRecipients = parseRecipientsFromUri(intent.data)
val extraAddress = intent.getStringExtra(ADDRESS)
val extraEmail = intent.getStringExtra(Intent.EXTRA_EMAIL)
val recipients = when {
!extraAddress.isNullOrEmpty() -> arrayOf(extraAddress)
!extraEmail.isNullOrEmpty() -> arrayOf(extraEmail)
else -> uriRecipients.orEmpty()
}
return recipients
.filter { it.length < MAX_RECIPIENT_LENGTH }
.joinToString(";")
}
private fun parseRecipientsFromUri(uri: Uri?): Array<String>? {
if (uri == null || uri.scheme !in SMS_MMS_SCHEMES) return null
val schemeSpecificPart = uri.schemeSpecificPart.split("?").firstOrNull() ?: return null
return schemeSpecificPart.replace(';', ',').split(",").toTypedArray()
}
private fun extractBodyFromIntent(intent: Intent): String? {
val uriBody = extractBodyFromUri(intent.data)
val smsBody = intent.getStringExtra(SMS_BODY)
val extraText = if (ContentType.TEXT_PLAIN == intent.type) {
intent.getStringExtra(Intent.EXTRA_TEXT)
} else {
// Invalid URL, probably
null
}
return smsBody ?: uriBody ?: extraText
}
private fun extractBodyFromUri(uri: Uri?): String? {
if (uri == null) return null
val query = uri.query ?: return null
val bodyParam = query.split("&").firstOrNull { it.startsWith("body=") } ?: return null
return try {
URLDecoder.decode(bodyParam.removePrefix("body="), "UTF-8")
} catch (e: UnsupportedEncodingException) {
null
}
}
}