diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index a78aca85..b3872b33 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -80,7 +80,6 @@ android:parentActivityName=".activities.MainActivity"> - diff --git a/app/src/main/kotlin/org/fossify/messages/activities/NewConversationActivity.kt b/app/src/main/kotlin/org/fossify/messages/activities/NewConversationActivity.kt index 5ea63e0a..25d42453 100644 --- a/app/src/main/kotlin/org/fossify/messages/activities/NewConversationActivity.kt +++ b/app/src/main/kotlin/org/fossify/messages/activities/NewConversationActivity.kt @@ -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() 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(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(Intent.EXTRA_STREAM) putExtra(THREAD_ATTACHMENT_URIS, uris) } diff --git a/app/src/main/kotlin/org/fossify/messages/helpers/SmsIntentParser.kt b/app/src/main/kotlin/org/fossify/messages/helpers/SmsIntentParser.kt new file mode 100644 index 00000000..a5a44dd3 --- /dev/null +++ b/app/src/main/kotlin/org/fossify/messages/helpers/SmsIntentParser.kt @@ -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? { + 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? { + 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 + } + } +}