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"> android:parentActivityName=".activities.MainActivity">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.SEND" />
<action android:name="android.intent.action.SENDTO" /> <action android:name="android.intent.action.SENDTO" />
<action android:name="android.intent.action.VIEW" /> <action android:name="android.intent.action.VIEW" />

View file

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