Merge pull request #543 from Naveen3Singh/rewrite_sms

Rewrite SMS related code into the app
This commit is contained in:
Tibor Kaputa
2023-01-10 13:29:47 +01:00
committed by GitHub
25 changed files with 1027 additions and 409 deletions

View File

@ -4,6 +4,7 @@ import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.view.WindowManager
import android.widget.Toast
import com.google.gson.Gson
import com.reddit.indicatorfastscroll.FastScrollItemIndicator
import com.simplemobiletools.commons.dialogs.RadioGroupDialog
@ -16,7 +17,7 @@ import com.simplemobiletools.smsmessenger.adapters.ContactsAdapter
import com.simplemobiletools.smsmessenger.extensions.getSuggestedContacts
import com.simplemobiletools.smsmessenger.extensions.getThreadId
import com.simplemobiletools.smsmessenger.helpers.*
import kotlinx.android.synthetic.main.activity_main.*
import com.simplemobiletools.smsmessenger.messaging.isShortCodeWithLetters
import kotlinx.android.synthetic.main.activity_new_conversation.*
import kotlinx.android.synthetic.main.item_suggested_contact.view.*
import java.net.URLDecoder
@ -79,6 +80,11 @@ class NewConversationActivity : SimpleActivity() {
new_conversation_confirm.applyColorFilter(getProperTextColor())
new_conversation_confirm.setOnClickListener {
val number = new_conversation_address.value
if (isShortCodeWithLetters(number)) {
new_conversation_address.setText("")
toast(R.string.invalid_short_code, length = Toast.LENGTH_LONG)
return@setOnClickListener
}
launchThreadActivity(number, number)
}

View File

@ -51,15 +51,19 @@ import com.simplemobiletools.smsmessenger.R
import com.simplemobiletools.smsmessenger.adapters.AttachmentsAdapter
import com.simplemobiletools.smsmessenger.adapters.AutoCompleteTextViewAdapter
import com.simplemobiletools.smsmessenger.adapters.ThreadAdapter
import com.simplemobiletools.smsmessenger.dialogs.InvalidNumberDialog
import com.simplemobiletools.smsmessenger.dialogs.RenameConversationDialog
import com.simplemobiletools.smsmessenger.dialogs.ScheduleMessageDialog
import com.simplemobiletools.smsmessenger.extensions.*
import com.simplemobiletools.smsmessenger.helpers.*
import com.simplemobiletools.smsmessenger.messaging.*
import com.simplemobiletools.smsmessenger.models.*
import com.simplemobiletools.smsmessenger.models.ThreadItem.*
import kotlinx.android.synthetic.main.activity_thread.*
import kotlinx.android.synthetic.main.item_selected_contact.view.*
import kotlinx.android.synthetic.main.layout_attachment_picker.*
import kotlinx.android.synthetic.main.layout_invalid_short_code_info.*
import kotlinx.android.synthetic.main.layout_thread_send_message_holder.*
import org.greenrobot.eventbus.EventBus
import org.greenrobot.eventbus.Subscribe
import org.greenrobot.eventbus.ThreadMode
@ -176,6 +180,7 @@ class ThreadActivity : SimpleActivity() {
}
thread_send_message_holder.setBackgroundColor(bottomBarColor)
reply_disabled_info_holder.setBackgroundColor(bottomBarColor)
updateNavigationBarColor(bottomBarColor)
}
@ -214,7 +219,8 @@ class ThreadActivity : SimpleActivity() {
findItem(R.id.conversation_details).isVisible = participants.size > 1 && conversation != null
findItem(R.id.block_number).title = addLockedLabelIfNeeded(R.string.block_number)
findItem(R.id.block_number).isVisible = isNougatPlus()
findItem(R.id.dial_number).isVisible = participants.size == 1
findItem(R.id.dial_number).isVisible = participants.size == 1 && !isSpecialNumber()
findItem(R.id.manage_people).isVisible = !isSpecialNumber()
findItem(R.id.mark_as_unread).isVisible = threadItems.isNotEmpty()
// allow saving number in cases when we dont have it stored yet and it is a casual readable number
@ -670,6 +676,35 @@ class ThreadActivity : SimpleActivity() {
} else {
messages.first().participants
}
runOnUiThread {
maybeDisableShortCodeReply()
}
}
}
private fun isSpecialNumber(): Boolean {
val addresses = participants.getAddresses()
return addresses.any { isShortCodeWithLetters(it) }
}
private fun maybeDisableShortCodeReply() {
if (isSpecialNumber()) {
thread_send_message_holder.beGone()
reply_disabled_info_holder.beVisible()
val textColor = getProperTextColor()
reply_disabled_text.setTextColor(textColor)
reply_disabled_info.apply {
applyColorFilter(textColor)
setOnClickListener {
InvalidNumberDialog(
activity = this@ThreadActivity,
text = getString(R.string.invalid_short_code_desc)
)
}
if (isOreoPlus()) {
tooltipText = getString(R.string.more_info)
}
}
}
}
@ -1167,7 +1202,7 @@ class ThreadActivity : SimpleActivity() {
try {
refreshedSinceSent = false
sendMessage(text, addresses, subscriptionId, attachments)
sendMessageCompat(text, addresses, subscriptionId, attachments)
ensureBackgroundThread {
val messageIds = messages.map { it.id }
val message = getMessages(threadId, getImageResolutions = true, limit = 1).firstOrNull { it.id !in messageIds }
@ -1347,7 +1382,7 @@ class ThreadActivity : SimpleActivity() {
private fun isMmsMessage(text: String): Boolean {
val isGroupMms = participants.size > 1 && config.sendGroupMessageMMS
val isLongMmsMessage = isLongMmsMessage(text) && config.sendLongMessageMMS
val isLongMmsMessage = isLongMmsMessage(text)
return getAttachmentSelections().isNotEmpty() || isGroupMms || isLongMmsMessage
}

View File

@ -27,6 +27,7 @@ import com.simplemobiletools.smsmessenger.activities.SimpleActivity
import com.simplemobiletools.smsmessenger.dialogs.RenameConversationDialog
import com.simplemobiletools.smsmessenger.extensions.*
import com.simplemobiletools.smsmessenger.helpers.refreshMessages
import com.simplemobiletools.smsmessenger.messaging.isShortCodeWithLetters
import com.simplemobiletools.smsmessenger.models.Conversation
import kotlinx.android.synthetic.main.item_conversation.view.*
@ -57,14 +58,17 @@ class ConversationsAdapter(
override fun prepareActionMode(menu: Menu) {
val selectedItems = getSelectedItems()
val isSingleSelection = isOneItemSelected()
val selectedConversation = selectedItems.firstOrNull()
val isGroupConversation = selectedConversation?.isGroupConversation == true
menu.apply {
findItem(R.id.cab_block_number).title = activity.addLockedLabelIfNeeded(R.string.block_number)
findItem(R.id.cab_block_number).isVisible = isNougatPlus()
findItem(R.id.cab_add_number_to_contact).isVisible = isOneItemSelected() && selectedItems.firstOrNull()?.isGroupConversation == false
findItem(R.id.cab_dial_number).isVisible = isOneItemSelected() && selectedItems.firstOrNull()?.isGroupConversation == false
findItem(R.id.cab_copy_number).isVisible = isOneItemSelected() && selectedItems.firstOrNull()?.isGroupConversation == false
findItem(R.id.rename_conversation).isVisible = isOneItemSelected() && selectedItems.firstOrNull()?.isGroupConversation == true
findItem(R.id.cab_add_number_to_contact).isVisible = isSingleSelection && !isGroupConversation
findItem(R.id.cab_dial_number).isVisible = isSingleSelection && !isGroupConversation && !isShortCodeWithLetters(selectedConversation!!.phoneNumber)
findItem(R.id.cab_copy_number).isVisible = isSingleSelection && !isGroupConversation
findItem(R.id.rename_conversation).isVisible = isSingleSelection && isGroupConversation
findItem(R.id.cab_mark_as_read).isVisible = selectedItems.any { !it.read }
findItem(R.id.cab_mark_as_unread).isVisible = selectedItems.any { it.read }
checkPinBtnVisibility(this)

View File

@ -0,0 +1,21 @@
package com.simplemobiletools.smsmessenger.dialogs
import com.simplemobiletools.commons.activities.BaseSimpleActivity
import com.simplemobiletools.commons.extensions.getAlertDialogBuilder
import com.simplemobiletools.commons.extensions.setupDialogStuff
import com.simplemobiletools.smsmessenger.R
import kotlinx.android.synthetic.main.dialog_invalid_number.view.*
class InvalidNumberDialog(val activity: BaseSimpleActivity, val text: String) {
init {
val view = activity.layoutInflater.inflate(R.layout.dialog_invalid_number, null).apply {
dialog_invalid_number_desc.text = text
}
activity.getAlertDialogBuilder()
.setPositiveButton(R.string.ok) { _, _ -> { } }
.apply {
activity.setupDialogStuff(view, this)
}
}
}

View File

@ -1,6 +1,7 @@
package com.simplemobiletools.smsmessenger.extensions
import android.annotation.SuppressLint
import android.app.Application
import android.content.ContentResolver
import android.content.ContentValues
import android.content.Context
@ -18,7 +19,6 @@ import android.text.TextUtils
import com.bumptech.glide.Glide
import com.bumptech.glide.load.engine.DiskCacheStrategy
import com.bumptech.glide.request.RequestOptions
import com.klinker.android.send_message.Transaction.getAddressSeparator
import com.simplemobiletools.commons.extensions.*
import com.simplemobiletools.commons.helpers.*
import com.simplemobiletools.commons.models.PhoneNumber
@ -31,6 +31,9 @@ import com.simplemobiletools.smsmessenger.interfaces.AttachmentsDao
import com.simplemobiletools.smsmessenger.interfaces.ConversationsDao
import com.simplemobiletools.smsmessenger.interfaces.MessageAttachmentsDao
import com.simplemobiletools.smsmessenger.interfaces.MessagesDao
import com.simplemobiletools.smsmessenger.messaging.MessagingUtils
import com.simplemobiletools.smsmessenger.messaging.MessagingUtils.Companion.ADDRESS_SEPARATOR
import com.simplemobiletools.smsmessenger.messaging.SmsSender
import com.simplemobiletools.smsmessenger.models.*
import me.leolin.shortcutbadger.ShortcutBadger
import java.io.FileNotFoundException
@ -49,6 +52,10 @@ val Context.messagesDB: MessagesDao get() = getMessagesDB().MessagesDao()
val Context.notificationHelper get() = NotificationHelper(this)
val Context.messagingUtils get() = MessagingUtils(this)
val Context.smsSender get() = SmsSender.getInstance(applicationContext as Application)
fun Context.getMessages(
threadId: Long,
getImageResolutions: Boolean,
@ -103,7 +110,7 @@ fun Context.getMessages(
val thread = cursor.getLongValue(Sms.THREAD_ID)
val subscriptionId = cursor.getIntValue(Sms.SUBSCRIPTION_ID)
val status = cursor.getIntValue(Sms.STATUS)
val participants = senderNumber.split(getAddressSeparator()).map { number ->
val participants = senderNumber.split(ADDRESS_SEPARATOR).map { number ->
val phoneNumber = PhoneNumber(number, 0, "", number)
val participantPhoto = getNameAndPhotoFromPhoneNumber(number)
SimpleContact(0, 0, participantPhoto.name, photoUri, arrayListOf(phoneNumber), ArrayList(), ArrayList())
@ -648,26 +655,6 @@ fun Context.markThreadMessagesUnread(threadId: Long) {
}
}
fun Context.updateMessageType(id: Long, type: Int) {
val uri = Sms.CONTENT_URI
val contentValues = ContentValues().apply {
put(Sms.TYPE, type)
}
val selection = "${Sms._ID} = ?"
val selectionArgs = arrayOf(id.toString())
contentResolver.update(uri, contentValues, selection, selectionArgs)
}
fun Context.updateMessageStatus(id: Long, status: Int) {
val uri = Sms.CONTENT_URI
val contentValues = ContentValues().apply {
put(Sms.STATUS, status)
}
val selection = "${Sms._ID} = ?"
val selectionArgs = arrayOf(id.toString())
contentResolver.update(uri, contentValues, selection, selectionArgs)
}
fun Context.updateUnreadCountBadge(conversations: List<Conversation>) {
val unreadCount = conversations.count { !it.read }
if (unreadCount == 0) {

View File

@ -2,6 +2,10 @@ package com.simplemobiletools.smsmessenger.helpers
import com.simplemobiletools.smsmessenger.models.Events
import org.greenrobot.eventbus.EventBus
import org.joda.time.DateTime
import org.joda.time.DateTimeZone
import kotlin.math.abs
import kotlin.random.Random
const val THREAD_ID = "thread_id"
const val THREAD_TITLE = "thread_title"
@ -81,3 +85,10 @@ const val PICK_CONTACT_INTENT = 48
fun refreshMessages() {
EventBus.getDefault().post(Events.RefreshMessages())
}
/** Not to be used with real messages persisted in the telephony db. This is for internal use only (e.g. scheduled messages, notification ids etc). */
fun generateRandomId(length: Int = 9): Long {
val millis = DateTime.now(DateTimeZone.UTC).millis
val random = abs(Random(millis).nextLong())
return random.toString().takeLast(length).toLong()
}

View File

@ -1,111 +0,0 @@
package com.simplemobiletools.smsmessenger.helpers
import android.app.AlarmManager
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import androidx.core.app.AlarmManagerCompat
import com.klinker.android.send_message.Settings
import com.klinker.android.send_message.Transaction
import com.klinker.android.send_message.Utils
import com.simplemobiletools.commons.extensions.showErrorToast
import com.simplemobiletools.smsmessenger.R
import com.simplemobiletools.smsmessenger.extensions.config
import com.simplemobiletools.smsmessenger.extensions.isPlainTextMimeType
import com.simplemobiletools.smsmessenger.models.Attachment
import com.simplemobiletools.smsmessenger.models.Message
import com.simplemobiletools.smsmessenger.receivers.ScheduledMessageReceiver
import com.simplemobiletools.smsmessenger.receivers.SmsStatusDeliveredReceiver
import com.simplemobiletools.smsmessenger.receivers.SmsStatusSentReceiver
import org.joda.time.DateTime
import org.joda.time.DateTimeZone
import kotlin.math.abs
import kotlin.random.Random
fun Context.getSendMessageSettings(): Settings {
val settings = Settings()
settings.useSystemSending = true
settings.deliveryReports = config.enableDeliveryReports
settings.sendLongAsMms = config.sendLongMessageMMS
settings.sendLongAsMmsAfter = 1
settings.group = config.sendGroupMessageMMS
return settings
}
fun Context.sendMessage(text: String, addresses: List<String>, subscriptionId: Int?, attachments: List<Attachment>) {
val settings = getSendMessageSettings()
if (subscriptionId != null) {
settings.subscriptionId = subscriptionId
}
val transaction = Transaction(this, settings)
val message = com.klinker.android.send_message.Message(text, addresses.toTypedArray())
if (attachments.isNotEmpty()) {
for (attachment in attachments) {
try {
val uri = attachment.getUri()
contentResolver.openInputStream(uri)?.use {
val bytes = it.readBytes()
val mimeType = if (attachment.mimetype.isPlainTextMimeType()) {
"application/txt"
} else {
attachment.mimetype
}
val name = attachment.filename
message.addMedia(bytes, mimeType, name, name)
}
} catch (e: Exception) {
showErrorToast(e)
} catch (e: Error) {
showErrorToast(e.localizedMessage ?: getString(R.string.unknown_error_occurred))
}
}
}
val smsSentIntent = Intent(this, SmsStatusSentReceiver::class.java)
val deliveredIntent = Intent(this, SmsStatusDeliveredReceiver::class.java)
transaction.setExplicitBroadcastForSentSms(smsSentIntent)
transaction.setExplicitBroadcastForDeliveredSms(deliveredIntent)
try {
transaction.sendNewMessage(message)
} catch (e: Exception) {
showErrorToast(e)
}
}
fun Context.getScheduleSendPendingIntent(message: Message): PendingIntent {
val intent = Intent(this, ScheduledMessageReceiver::class.java)
intent.putExtra(THREAD_ID, message.threadId)
intent.putExtra(SCHEDULED_MESSAGE_ID, message.id)
val flags = PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
return PendingIntent.getBroadcast(this, message.id.toInt(), intent, flags)
}
fun Context.scheduleMessage(message: Message) {
val pendingIntent = getScheduleSendPendingIntent(message)
val triggerAtMillis = message.millis()
val alarmManager = getSystemService(Context.ALARM_SERVICE) as AlarmManager
AlarmManagerCompat.setExactAndAllowWhileIdle(alarmManager, AlarmManager.RTC_WAKEUP, triggerAtMillis, pendingIntent)
}
fun Context.cancelScheduleSendPendingIntent(messageId: Long) {
val intent = Intent(this, ScheduledMessageReceiver::class.java)
val flags = PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
PendingIntent.getBroadcast(this, messageId.toInt(), intent, flags).cancel()
}
fun Context.isLongMmsMessage(text: String): Boolean {
val settings = getSendMessageSettings()
return Utils.getNumPages(settings, text) > settings.sendLongAsMmsAfter
}
/** Not to be used with real messages persisted in the telephony db. This is for internal use only (e.g. scheduled messages). */
fun generateRandomId(length: Int = 9): Long {
val millis = DateTime.now(DateTimeZone.UTC).millis
val random = abs(Random(millis).nextLong())
return random.toString().takeLast(length).toLong()
}

View File

@ -22,6 +22,7 @@ import com.simplemobiletools.commons.helpers.isOreoPlus
import com.simplemobiletools.smsmessenger.R
import com.simplemobiletools.smsmessenger.activities.ThreadActivity
import com.simplemobiletools.smsmessenger.extensions.config
import com.simplemobiletools.smsmessenger.messaging.isShortCodeWithLetters
import com.simplemobiletools.smsmessenger.receivers.DirectReplyReceiver
import com.simplemobiletools.smsmessenger.receivers.MarkAsReadReceiver
@ -52,7 +53,7 @@ class NotificationHelper(private val context: Context) {
PendingIntent.getBroadcast(context, notificationId, markAsReadIntent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_MUTABLE)
var replyAction: NotificationCompat.Action? = null
if (isNougatPlus()) {
if (isNougatPlus() && !isShortCodeWithLetters(address)) {
val replyLabel = context.getString(R.string.reply)
val remoteInput = RemoteInput.Builder(REPLY)
.setLabel(replyLabel)

View File

@ -0,0 +1,71 @@
package com.simplemobiletools.smsmessenger.messaging
import android.content.Context
import android.telephony.SmsMessage
import android.widget.Toast.LENGTH_LONG
import com.klinker.android.send_message.Settings
import com.simplemobiletools.commons.extensions.showErrorToast
import com.simplemobiletools.commons.extensions.toast
import com.simplemobiletools.smsmessenger.R
import com.simplemobiletools.smsmessenger.extensions.config
import com.simplemobiletools.smsmessenger.extensions.messagingUtils
import com.simplemobiletools.smsmessenger.messaging.SmsException.Companion.EMPTY_DESTINATION_ADDRESS
import com.simplemobiletools.smsmessenger.messaging.SmsException.Companion.ERROR_PERSISTING_MESSAGE
import com.simplemobiletools.smsmessenger.messaging.SmsException.Companion.ERROR_SENDING_MESSAGE
import com.simplemobiletools.smsmessenger.models.Attachment
@Deprecated("TODO: Move/rewrite messaging config code into the app.")
fun Context.getSendMessageSettings(): Settings {
val settings = Settings()
settings.useSystemSending = true
settings.deliveryReports = config.enableDeliveryReports
settings.sendLongAsMms = config.sendLongMessageMMS
settings.sendLongAsMmsAfter = 1
settings.group = config.sendGroupMessageMMS
return settings
}
fun Context.isLongMmsMessage(text: String, settings: Settings = getSendMessageSettings()): Boolean {
val data = SmsMessage.calculateLength(text, false)
val numPages = data.first()
return numPages > settings.sendLongAsMmsAfter && settings.sendLongAsMms
}
/** Sends the message using the in-app SmsManager API wrappers if it's an SMS or using android-smsmms for MMS. */
fun Context.sendMessageCompat(text: String, addresses: List<String>, subId: Int?, attachments: List<Attachment>) {
val settings = getSendMessageSettings()
if (subId != null) {
settings.subscriptionId = subId
}
val isMms = attachments.isNotEmpty() || isLongMmsMessage(text, settings) || addresses.size > 1 && settings.group
if (isMms) {
messagingUtils.sendMmsMessage(text, addresses, attachments, settings)
} else {
try {
messagingUtils.sendSmsMessage(text, addresses.toSet(), settings.subscriptionId, settings.deliveryReports)
} catch (e: SmsException) {
when (e.errorCode) {
EMPTY_DESTINATION_ADDRESS -> toast(id = R.string.empty_destination_address, length = LENGTH_LONG)
ERROR_PERSISTING_MESSAGE -> toast(id = R.string.unable_to_save_message, length = LENGTH_LONG)
ERROR_SENDING_MESSAGE -> toast(
msg = getString(R.string.unknown_error_occurred_sending_message, e.errorCode),
length = LENGTH_LONG
)
}
} catch (e: Exception) {
showErrorToast(e)
}
}
}
/**
* Check if a given "address" is a short code.
* There's not much info available on these special numbers, even the wikipedia page (https://en.wikipedia.org/wiki/Short_code)
* contains outdated information regarding max number of digits. The exact parameters for short codes can vary by country and by carrier.
*
* This function simply returns true if the [address] contains at least one letter.
*/
fun isShortCodeWithLetters(address: String): Boolean {
return address.any { it.isLetter() }
}

View File

@ -0,0 +1,192 @@
package com.simplemobiletools.smsmessenger.messaging
import android.annotation.SuppressLint
import android.app.Activity
import android.content.ContentValues
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.provider.Telephony.Sms
import android.telephony.SmsManager
import android.telephony.SmsMessage
import android.widget.Toast
import com.klinker.android.send_message.Message
import com.klinker.android.send_message.Settings
import com.klinker.android.send_message.Transaction
import com.simplemobiletools.commons.extensions.showErrorToast
import com.simplemobiletools.commons.extensions.toast
import com.simplemobiletools.smsmessenger.R
import com.simplemobiletools.smsmessenger.extensions.getThreadId
import com.simplemobiletools.smsmessenger.extensions.isPlainTextMimeType
import com.simplemobiletools.smsmessenger.extensions.smsSender
import com.simplemobiletools.smsmessenger.messaging.SmsException.Companion.ERROR_PERSISTING_MESSAGE
import com.simplemobiletools.smsmessenger.models.Attachment
import com.simplemobiletools.smsmessenger.receivers.MmsSentReceiver
import com.simplemobiletools.smsmessenger.receivers.SendStatusReceiver
class MessagingUtils(val context: Context) {
/**
* Insert an SMS to the given URI with thread_id specified.
*/
private fun insertSmsMessage(
subId: Int, dest: String, text: String, timestamp: Long, threadId: Long,
status: Int = Sms.STATUS_NONE, type: Int = Sms.MESSAGE_TYPE_OUTBOX
): Uri {
val response: Uri?
val values = ContentValues().apply {
put(Sms.ADDRESS, dest)
put(Sms.DATE, timestamp)
put(Sms.READ, 1)
put(Sms.SEEN, 1)
put(Sms.BODY, text)
// insert subscription id only if it is a valid one.
if (subId != Settings.DEFAULT_SUBSCRIPTION_ID) {
put(Sms.SUBSCRIPTION_ID, subId)
}
if (status != Sms.STATUS_NONE) {
put(Sms.STATUS, status)
}
if (type != Sms.MESSAGE_TYPE_ALL) {
put(Sms.TYPE, type)
}
if (threadId != -1L) {
put(Sms.THREAD_ID, threadId)
}
}
try {
response = context.contentResolver.insert(Sms.CONTENT_URI, values)
} catch (e: Exception) {
throw SmsException(ERROR_PERSISTING_MESSAGE, e)
}
return response ?: throw SmsException(ERROR_PERSISTING_MESSAGE)
}
/** Send an SMS message given [text] and [addresses]. A [SmsException] is thrown in case any errors occur. */
fun sendSmsMessage(
text: String, addresses: Set<String>, subId: Int, requireDeliveryReport: Boolean
) {
if (addresses.size > 1) {
// insert a dummy message for this thread if it is a group message
val broadCastThreadId = context.getThreadId(addresses.toSet())
val mergedAddresses = addresses.joinToString(ADDRESS_SEPARATOR)
insertSmsMessage(
subId = subId, dest = mergedAddresses, text = text,
timestamp = System.currentTimeMillis(), threadId = broadCastThreadId,
status = Sms.Sent.STATUS_COMPLETE, type = Sms.Sent.MESSAGE_TYPE_SENT
)
}
for (address in addresses) {
val threadId = context.getThreadId(address)
val messageUri = insertSmsMessage(
subId = subId, dest = address, text = text,
timestamp = System.currentTimeMillis(), threadId = threadId
)
try {
context.smsSender.sendMessage(
subId = subId, destination = address, body = text, serviceCenter = null,
requireDeliveryReport = requireDeliveryReport, messageUri = messageUri
)
} catch (e: Exception) {
updateSmsMessageSendingStatus(messageUri, Sms.Outbox.MESSAGE_TYPE_FAILED)
throw e // propagate error to caller
}
}
}
fun updateSmsMessageSendingStatus(messageUri: Uri?, type: Int) {
val resolver = context.contentResolver
val values = ContentValues().apply {
put(Sms.Outbox.TYPE, type)
}
try {
if (messageUri != null) {
resolver.update(messageUri, values, null, null)
} else {
// mark latest sms as sent, need to check if this is still necessary (or reliable)
// as this was taken from android-smsmms. The messageUri shouldn't be null anyway
val cursor = resolver.query(Sms.Outbox.CONTENT_URI, null, null, null, null)
cursor?.use {
if (cursor.moveToFirst()) {
@SuppressLint("Range")
val id = cursor.getString(cursor.getColumnIndex(Sms.Outbox._ID))
val selection = "${Sms._ID} = ?"
val selectionArgs = arrayOf(id.toString())
resolver.update(Sms.Outbox.CONTENT_URI, values, selection, selectionArgs)
}
}
}
} catch (e: Exception) {
context.showErrorToast(e)
}
}
fun getSmsMessageFromDeliveryReport(intent: Intent): SmsMessage? {
val pdu = intent.getByteArrayExtra("pdu")
val format = intent.getStringExtra("format")
return SmsMessage.createFromPdu(pdu, format)
}
@Deprecated("TODO: Move/rewrite MMS code into the app.")
fun sendMmsMessage(text: String, addresses: List<String>, attachments: List<Attachment>, settings: Settings) {
val transaction = Transaction(context, settings)
val message = Message(text, addresses.toTypedArray())
if (attachments.isNotEmpty()) {
for (attachment in attachments) {
try {
val uri = attachment.getUri()
context.contentResolver.openInputStream(uri)?.use {
val bytes = it.readBytes()
val mimeType = if (attachment.mimetype.isPlainTextMimeType()) {
"application/txt"
} else {
attachment.mimetype
}
val name = attachment.filename
message.addMedia(bytes, mimeType, name, name)
}
} catch (e: Exception) {
context.showErrorToast(e)
} catch (e: Error) {
context.showErrorToast(e.localizedMessage ?: context.getString(R.string.unknown_error_occurred))
}
}
}
val mmsSentIntent = Intent(context, MmsSentReceiver::class.java)
transaction.setExplicitBroadcastForSentMms(mmsSentIntent)
try {
transaction.sendNewMessage(message)
} catch (e: Exception) {
context.showErrorToast(e)
}
}
fun maybeShowErrorToast(resultCode: Int, errorCode: Int) {
if (resultCode != Activity.RESULT_OK) {
val msg = if (errorCode != SendStatusReceiver.NO_ERROR_CODE) {
context.getString(R.string.carrier_send_error)
} else {
when (resultCode) {
SmsManager.RESULT_ERROR_NO_SERVICE -> context.getString(R.string.error_service_is_unavailable)
SmsManager.RESULT_ERROR_RADIO_OFF -> context.getString(R.string.error_radio_turned_off)
else -> context.getString(R.string.unknown_error_occurred_sending_message, resultCode)
}
}
context.toast(msg = msg, length = Toast.LENGTH_LONG)
} else {
// no-op
}
}
companion object {
const val ADDRESS_SEPARATOR = "|"
}
}

View File

@ -0,0 +1,38 @@
package com.simplemobiletools.smsmessenger.messaging
import android.app.AlarmManager
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import androidx.core.app.AlarmManagerCompat
import com.simplemobiletools.smsmessenger.helpers.SCHEDULED_MESSAGE_ID
import com.simplemobiletools.smsmessenger.helpers.THREAD_ID
import com.simplemobiletools.smsmessenger.models.Message
import com.simplemobiletools.smsmessenger.receivers.ScheduledMessageReceiver
/**
* All things related to scheduled messages are here.
*/
fun Context.getScheduleSendPendingIntent(message: Message): PendingIntent {
val intent = Intent(this, ScheduledMessageReceiver::class.java)
intent.putExtra(THREAD_ID, message.threadId)
intent.putExtra(SCHEDULED_MESSAGE_ID, message.id)
val flags = PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
return PendingIntent.getBroadcast(this, message.id.toInt(), intent, flags)
}
fun Context.scheduleMessage(message: Message) {
val pendingIntent = getScheduleSendPendingIntent(message)
val triggerAtMillis = message.millis()
val alarmManager = getSystemService(Context.ALARM_SERVICE) as AlarmManager
AlarmManagerCompat.setExactAndAllowWhileIdle(alarmManager, AlarmManager.RTC_WAKEUP, triggerAtMillis, pendingIntent)
}
fun Context.cancelScheduleSendPendingIntent(messageId: Long) {
val intent = Intent(this, ScheduledMessageReceiver::class.java)
val flags = PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
PendingIntent.getBroadcast(this, messageId.toInt(), intent, flags).cancel()
}

View File

@ -0,0 +1,9 @@
package com.simplemobiletools.smsmessenger.messaging
class SmsException(val errorCode: Int, val exception: Exception? = null) : Exception() {
companion object {
const val EMPTY_DESTINATION_ADDRESS = -1
const val ERROR_PERSISTING_MESSAGE = -2
const val ERROR_SENDING_MESSAGE = -3
}
}

View File

@ -0,0 +1,25 @@
package com.simplemobiletools.smsmessenger.messaging
import android.telephony.SmsManager
import com.klinker.android.send_message.Settings
private var smsManagerInstance: SmsManager? = null
private var associatedSubId: Int = -1
@Suppress("DEPRECATION")
fun getSmsManager(subId: Int): SmsManager {
if (smsManagerInstance == null || subId != associatedSubId) {
smsManagerInstance = if (subId != Settings.DEFAULT_SUBSCRIPTION_ID) {
try {
smsManagerInstance = SmsManager.getSmsManagerForSubscriptionId(subId)
} catch (e: Exception) {
e.printStackTrace()
}
smsManagerInstance ?: SmsManager.getDefault()
} else {
SmsManager.getDefault()
}
associatedSubId = subId
}
return smsManagerInstance!!
}

View File

@ -0,0 +1,133 @@
package com.simplemobiletools.smsmessenger.messaging
import android.app.Application
import android.app.PendingIntent
import android.content.Intent
import android.net.Uri
import android.telephony.PhoneNumberUtils
import com.simplemobiletools.commons.helpers.isSPlus
import com.simplemobiletools.smsmessenger.messaging.SmsException.Companion.EMPTY_DESTINATION_ADDRESS
import com.simplemobiletools.smsmessenger.messaging.SmsException.Companion.ERROR_SENDING_MESSAGE
import com.simplemobiletools.smsmessenger.receivers.SendStatusReceiver
import com.simplemobiletools.smsmessenger.receivers.SmsStatusDeliveredReceiver
import com.simplemobiletools.smsmessenger.receivers.SmsStatusSentReceiver
/** Class that sends chat message via SMS. */
class SmsSender(val app: Application) {
// not sure what to do about this yet. this is the default as per android-smsmms
private val sendMultipartSmsAsSeparateMessages = false
// This should be called from a RequestWriter queue thread
fun sendMessage(
subId: Int, destination: String, body: String, serviceCenter: String?,
requireDeliveryReport: Boolean, messageUri: Uri
) {
var dest = destination
if (body.isEmpty()) {
throw IllegalArgumentException("SmsSender: empty text message")
}
// remove spaces and dashes from destination number
// (e.g. "801 555 1212" -> "8015551212")
// (e.g. "+8211-123-4567" -> "+82111234567")
dest = PhoneNumberUtils.stripSeparators(dest)
if (dest.isEmpty()) {
throw SmsException(EMPTY_DESTINATION_ADDRESS)
}
// Divide the input message by SMS length limit
val smsManager = getSmsManager(subId)
val messages = smsManager.divideMessage(body)
if (messages == null || messages.size < 1) {
throw SmsException(ERROR_SENDING_MESSAGE)
}
// Actually send the sms
sendInternal(
subId, dest, messages, serviceCenter, requireDeliveryReport, messageUri
)
}
// Actually sending the message using SmsManager
private fun sendInternal(
subId: Int, dest: String,
messages: ArrayList<String>, serviceCenter: String?,
requireDeliveryReport: Boolean, messageUri: Uri
) {
val smsManager = getSmsManager(subId)
val messageCount = messages.size
val deliveryIntents = ArrayList<PendingIntent?>(messageCount)
val sentIntents = ArrayList<PendingIntent>(messageCount)
var flags = PendingIntent.FLAG_UPDATE_CURRENT
if (isSPlus()) {
flags = flags or PendingIntent.FLAG_MUTABLE
}
for (i in 0 until messageCount) {
// Make pending intents different for each message part
val partId = if (messageCount <= 1) 0 else i + 1
if (requireDeliveryReport && i == messageCount - 1) {
deliveryIntents.add(
PendingIntent.getBroadcast(
app,
partId,
getDeliveredStatusIntent(messageUri, subId),
flags
)
)
} else {
deliveryIntents.add(null)
}
sentIntents.add(
PendingIntent.getBroadcast(
app,
partId,
getSendStatusIntent(messageUri, subId),
flags
)
)
}
try {
if (sendMultipartSmsAsSeparateMessages) {
// If multipart sms is not supported, send them as separate messages
for (i in 0 until messageCount) {
smsManager.sendTextMessage(
dest,
serviceCenter,
messages[i],
sentIntents[i],
deliveryIntents[i]
)
}
} else {
smsManager.sendMultipartTextMessage(
dest, serviceCenter, messages, sentIntents, deliveryIntents
)
}
} catch (e: Exception) {
throw SmsException(ERROR_SENDING_MESSAGE, e)
}
}
private fun getSendStatusIntent(requestUri: Uri, subId: Int): Intent {
val intent = Intent(SendStatusReceiver.SMS_SENT_ACTION, requestUri, app, SmsStatusSentReceiver::class.java)
intent.putExtra(SendStatusReceiver.EXTRA_SUB_ID, subId)
return intent
}
private fun getDeliveredStatusIntent(requestUri: Uri, subId: Int): Intent {
val intent = Intent(SendStatusReceiver.SMS_DELIVERED_ACTION, requestUri, app, SmsStatusDeliveredReceiver::class.java)
intent.putExtra(SendStatusReceiver.EXTRA_SUB_ID, subId)
return intent
}
companion object {
private var instance: SmsSender? = null
fun getInstance(app: Application): SmsSender {
if (instance == null) {
instance = SmsSender(app)
}
return instance!!
}
}
}

View File

@ -14,7 +14,7 @@ import com.simplemobiletools.smsmessenger.extensions.*
import com.simplemobiletools.smsmessenger.helpers.REPLY
import com.simplemobiletools.smsmessenger.helpers.THREAD_ID
import com.simplemobiletools.smsmessenger.helpers.THREAD_NUMBER
import com.simplemobiletools.smsmessenger.helpers.sendMessage
import com.simplemobiletools.smsmessenger.messaging.sendMessageCompat
class DirectReplyReceiver : BroadcastReceiver() {
@SuppressLint("MissingPermission")
@ -38,7 +38,7 @@ class DirectReplyReceiver : BroadcastReceiver() {
ensureBackgroundThread {
try {
context.sendMessage(body, listOf(address), subscriptionId, emptyList())
context.sendMessageCompat(body, listOf(address), subscriptionId, emptyList())
val message = context.getMessages(threadId, getImageResolutions = false, includeScheduledMessages = false, limit = 1).lastOrNull()
if (message != null) {
context.messagesDB.insertOrUpdate(message)

View File

@ -1,15 +1,54 @@
package com.simplemobiletools.smsmessenger.receivers
import android.app.Activity
import android.content.ContentValues
import android.content.Context
import android.content.Intent
import android.database.sqlite.SQLiteException
import android.net.Uri
import android.provider.Telephony
import android.widget.Toast
import com.simplemobiletools.commons.extensions.showErrorToast
import com.simplemobiletools.commons.extensions.toast
import com.simplemobiletools.smsmessenger.R
import com.simplemobiletools.smsmessenger.helpers.refreshMessages
import java.io.File
class MmsSentReceiver : com.klinker.android.send_message.MmsSentReceiver() {
override fun onMessageStatusUpdated(context: Context?, intent: Intent?, resultCode: Int) {
super.onMessageStatusUpdated(context, intent, resultCode)
/** Handles updating databases and states when a MMS message is sent. */
class MmsSentReceiver : SendStatusReceiver() {
override fun updateAndroidDatabase(context: Context, intent: Intent, receiverResultCode: Int) {
val uri = Uri.parse(intent.getStringExtra(EXTRA_CONTENT_URI))
val messageBox = if (receiverResultCode == Activity.RESULT_OK) {
Telephony.Mms.MESSAGE_BOX_SENT
} else {
val msg = context.getString(R.string.unknown_error_occurred_sending_message, receiverResultCode)
context.toast(msg = msg, length = Toast.LENGTH_LONG)
Telephony.Mms.MESSAGE_BOX_FAILED
}
val values = ContentValues(1).apply {
put(Telephony.Mms.MESSAGE_BOX, messageBox)
}
try {
context.contentResolver.update(uri, values, null, null)
} catch (e: SQLiteException) {
context.showErrorToast(e)
}
val filePath = intent.getStringExtra(EXTRA_FILE_PATH)
if (filePath != null) {
File(filePath).delete()
}
}
override fun updateAppDatabase(context: Context, intent: Intent, receiverResultCode: Int) {
if (resultCode == Activity.RESULT_OK) {
refreshMessages()
}
}
companion object {
private const val EXTRA_CONTENT_URI = "content_uri"
private const val EXTRA_FILE_PATH = "file_path"
}
}

View File

@ -16,7 +16,7 @@ import com.simplemobiletools.smsmessenger.extensions.messagesDB
import com.simplemobiletools.smsmessenger.helpers.SCHEDULED_MESSAGE_ID
import com.simplemobiletools.smsmessenger.helpers.THREAD_ID
import com.simplemobiletools.smsmessenger.helpers.refreshMessages
import com.simplemobiletools.smsmessenger.helpers.sendMessage
import com.simplemobiletools.smsmessenger.messaging.sendMessageCompat
class ScheduledMessageReceiver : BroadcastReceiver() {
@ -46,7 +46,7 @@ class ScheduledMessageReceiver : BroadcastReceiver() {
try {
Handler(Looper.getMainLooper()).post {
context.sendMessage(message.body, addresses, message.subscriptionId, attachments)
context.sendMessageCompat(message.body, addresses, message.subscriptionId, attachments)
}
// delete temporary conversation and message as it's already persisted to the telephony db now

View File

@ -0,0 +1,33 @@
package com.simplemobiletools.smsmessenger.receivers
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import com.simplemobiletools.commons.helpers.ensureBackgroundThread
abstract class SendStatusReceiver : BroadcastReceiver() {
// Updates the status of the message in the internal database
abstract fun updateAndroidDatabase(context: Context, intent: Intent, receiverResultCode: Int)
// allows the implementer to update the status of the message in their database
abstract fun updateAppDatabase(context: Context, intent: Intent, receiverResultCode: Int)
override fun onReceive(context: Context, intent: Intent) {
val resultCode = resultCode
ensureBackgroundThread {
updateAndroidDatabase(context, intent, resultCode)
updateAppDatabase(context, intent, resultCode)
}
}
companion object {
const val SMS_SENT_ACTION = "com.simplemobiletools.smsmessenger.receiver.SMS_SENT"
const val SMS_DELIVERED_ACTION = "com.simplemobiletools.smsmessenger.receiver.SMS_DELIVERED"
// Defined by platform, but no constant provided. See docs for SmsManager.sendTextMessage.
const val EXTRA_ERROR_CODE = "errorCode"
const val EXTRA_SUB_ID = "subId"
const val NO_ERROR_CODE = -1
}
}

View File

@ -1,35 +1,95 @@
package com.simplemobiletools.smsmessenger.receivers
import android.annotation.SuppressLint
import android.content.ContentValues
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Handler
import android.os.Looper
import android.provider.Telephony
import com.klinker.android.send_message.DeliveredReceiver
import android.provider.Telephony.Sms
import com.simplemobiletools.commons.helpers.ensureBackgroundThread
import com.simplemobiletools.smsmessenger.extensions.messagesDB
import com.simplemobiletools.smsmessenger.extensions.updateMessageStatus
import com.simplemobiletools.smsmessenger.extensions.messagingUtils
import com.simplemobiletools.smsmessenger.helpers.refreshMessages
class SmsStatusDeliveredReceiver : DeliveredReceiver() {
/** Handles updating databases and states when a sent SMS message is delivered. */
class SmsStatusDeliveredReceiver : SendStatusReceiver() {
override fun onMessageStatusUpdated(context: Context, intent: Intent, receiverResultCode: Int) {
if (intent.extras?.containsKey("message_uri") == true) {
val uri = Uri.parse(intent.getStringExtra("message_uri"))
val messageId = uri?.lastPathSegment?.toLong() ?: 0L
ensureBackgroundThread {
val status = Telephony.Sms.STATUS_COMPLETE
context.updateMessageStatus(messageId, status)
val updated = context.messagesDB.updateStatus(messageId, status)
if (updated == 0) {
Handler(Looper.getMainLooper()).postDelayed({
private var status: Int = Sms.Sent.STATUS_NONE
override fun updateAndroidDatabase(context: Context, intent: Intent, receiverResultCode: Int) {
val messageUri: Uri? = intent.data
val smsMessage = context.messagingUtils.getSmsMessageFromDeliveryReport(intent) ?: return
try {
val format = intent.getStringExtra("format")
status = smsMessage.status
// Simple matching up CDMA status with GSM status.
if ("3gpp2" == format) {
val errorClass = status shr 24 and 0x03
val statusCode = status shr 16 and 0x3f
status = when (errorClass) {
0 -> {
if (statusCode == 0x02 /*STATUS_DELIVERED*/) {
Sms.STATUS_COMPLETE
} else {
Sms.STATUS_PENDING
}
}
2 -> {
// TODO: Need to check whether SC still trying to deliver the SMS to destination and will send the report again?
Sms.STATUS_PENDING
}
3 -> {
Sms.STATUS_FAILED
}
else -> {
Sms.STATUS_PENDING
}
}
}
} catch (e: NullPointerException) {
// Sometimes, SmsMessage.mWrappedSmsMessage is null causing NPE when we access
// the methods on it although the SmsMessage itself is not null.
return
}
updateSmsStatusAndDateSent(context, messageUri, System.currentTimeMillis())
}
private fun updateSmsStatusAndDateSent(context: Context, messageUri: Uri?, timeSentInMillis: Long = -1L) {
val resolver = context.contentResolver
val values = ContentValues().apply {
if (status != Sms.Sent.STATUS_NONE) {
put(Sms.Sent.STATUS, status)
}
put(Sms.Sent.DATE_SENT, timeSentInMillis)
}
if (messageUri != null) {
resolver.update(messageUri, values, null, null)
} else {
// mark latest sms as delivered, need to check if this is still necessary (or reliable)
val cursor = resolver.query(Sms.Sent.CONTENT_URI, null, null, null, "date desc")
cursor?.use {
if (cursor.moveToFirst()) {
@SuppressLint("Range")
val id = cursor.getString(cursor.getColumnIndex(Sms.Sent._ID))
val selection = "${Sms._ID} = ?"
val selectionArgs = arrayOf(id.toString())
resolver.update(Sms.Sent.CONTENT_URI, values, selection, selectionArgs)
}
}
}
}
override fun updateAppDatabase(context: Context, intent: Intent, receiverResultCode: Int) {
val messageUri: Uri? = intent.data
if (messageUri != null) {
val messageId = messageUri.lastPathSegment?.toLong() ?: 0L
ensureBackgroundThread {
if (status != Sms.Sent.STATUS_NONE) {
context.messagesDB.updateStatus(messageId, status)
}
}, 2000)
}
refreshMessages()
}
}

View File

@ -6,39 +6,47 @@ import android.content.Intent
import android.net.Uri
import android.os.Handler
import android.os.Looper
import android.provider.Telephony
import android.provider.Telephony.Sms
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.ProcessLifecycleOwner
import com.klinker.android.send_message.SentReceiver
import com.simplemobiletools.commons.extensions.getMyContactsCursor
import com.simplemobiletools.commons.helpers.ensureBackgroundThread
import com.simplemobiletools.smsmessenger.extensions.*
import com.simplemobiletools.smsmessenger.helpers.refreshMessages
class SmsStatusSentReceiver : SentReceiver() {
/** Handles updating databases and states when a SMS message is sent. */
class SmsStatusSentReceiver : SendStatusReceiver() {
override fun onMessageStatusUpdated(context: Context, intent: Intent, receiverResultCode: Int) {
if (intent.extras?.containsKey("message_uri") == true) {
val uri = Uri.parse(intent.getStringExtra("message_uri"))
val messageId = uri?.lastPathSegment?.toLong() ?: 0L
override fun updateAndroidDatabase(context: Context, intent: Intent, receiverResultCode: Int) {
val messageUri: Uri? = intent.data
val resultCode = resultCode
val messagingUtils = context.messagingUtils
val type = if (resultCode == Activity.RESULT_OK) {
Sms.MESSAGE_TYPE_SENT
} else {
Sms.MESSAGE_TYPE_FAILED
}
messagingUtils.updateSmsMessageSendingStatus(messageUri, type)
messagingUtils.maybeShowErrorToast(
resultCode = resultCode,
errorCode = intent.getIntExtra(EXTRA_ERROR_CODE, NO_ERROR_CODE)
)
}
override fun updateAppDatabase(context: Context, intent: Intent, receiverResultCode: Int) {
val messageUri = intent.data
if (messageUri != null) {
val messageId = messageUri.lastPathSegment?.toLong() ?: 0L
ensureBackgroundThread {
val type = if (receiverResultCode == Activity.RESULT_OK) {
Telephony.Sms.MESSAGE_TYPE_SENT
Sms.MESSAGE_TYPE_SENT
} else {
showSendingFailedNotification(context, messageId)
Telephony.Sms.MESSAGE_TYPE_FAILED
Sms.MESSAGE_TYPE_FAILED
}
context.updateMessageType(messageId, type)
val updated = context.messagesDB.updateType(messageId, type)
if (updated == 0) {
Handler(Looper.getMainLooper()).postDelayed({
ensureBackgroundThread {
context.messagesDB.updateType(messageId, type)
}
}, 2000)
}
refreshMessages()
}
}

View File

@ -3,10 +3,8 @@ package com.simplemobiletools.smsmessenger.services
import android.app.Service
import android.content.Intent
import android.net.Uri
import com.klinker.android.send_message.Transaction
import com.simplemobiletools.smsmessenger.helpers.getSendMessageSettings
import com.simplemobiletools.smsmessenger.receivers.SmsStatusDeliveredReceiver
import com.simplemobiletools.smsmessenger.receivers.SmsStatusSentReceiver
import com.klinker.android.send_message.Settings
import com.simplemobiletools.smsmessenger.messaging.sendMessageCompat
class HeadlessSmsSendService : Service() {
override fun onBind(intent: Intent?) = null
@ -19,17 +17,11 @@ class HeadlessSmsSendService : Service() {
val number = Uri.decode(intent.dataString!!.removePrefix("sms:").removePrefix("smsto:").removePrefix("mms").removePrefix("mmsto:").trim())
val text = intent.getStringExtra(Intent.EXTRA_TEXT)
val settings = getSendMessageSettings()
val transaction = Transaction(this, settings)
val message = com.klinker.android.send_message.Message(text, number)
val smsSentIntent = Intent(this, SmsStatusSentReceiver::class.java)
val deliveredIntent = Intent(this, SmsStatusDeliveredReceiver::class.java)
transaction.setExplicitBroadcastForSentSms(smsSentIntent)
transaction.setExplicitBroadcastForDeliveredSms(deliveredIntent)
transaction.sendNewMessage(message)
if (!text.isNullOrEmpty()) {
val addresses = listOf(number)
val subId = Settings.DEFAULT_SUBSCRIPTION_ID
sendMessageCompat(text, addresses, subId, emptyList())
}
} catch (ignored: Exception) {
}

View File

@ -102,7 +102,7 @@
android:id="@+id/thread_messages_fastscroller"
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintBottom_toTopOf="@id/thread_send_message_holder"
app:layout_constraintBottom_toTopOf="@id/reply_disabled_info_holder"
app:layout_constraintTop_toBottomOf="@id/thread_add_contacts"
app:supportSwipeToRefresh="true">
@ -112,7 +112,7 @@
android:layout_height="match_parent"
android:clipToPadding="false"
android:overScrollMode="ifContentScrolls"
android:paddingBottom="@dimen/small_margin"
android:paddingBottom="@dimen/medium_margin"
android:scrollbars="none"
app:layoutManager="com.simplemobiletools.commons.views.MyLinearLayoutManager"
app:stackFromEnd="true"
@ -121,215 +121,24 @@
</com.qtalk.recyclerviewfastscroller.RecyclerViewFastScroller>
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/thread_send_message_holder"
<include
layout="@layout/layout_invalid_short_code_info"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent">
<RelativeLayout
android:id="@+id/scheduled_message_holder"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/normal_margin"
android:layout_marginTop="@dimen/medium_margin"
android:layout_marginEnd="@dimen/medium_margin"
android:background="@drawable/section_holder_stroke"
android:orientation="horizontal"
android:visibility="gone"
app:layout_constraintBottom_toTopOf="@id/thread_attachments_recyclerview"
app:layout_constraintBottom_toTopOf="@id/thread_send_message_holder"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_goneMarginBottom="@dimen/medium_margin"
tools:visibility="visible">
<ImageView
android:id="@+id/scheduled_message_icon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignTop="@+id/scheduled_message_button"
android:layout_alignBottom="@+id/scheduled_message_button"
android:paddingTop="@dimen/medium_margin"
android:paddingBottom="@dimen/medium_margin"
android:src="@drawable/ic_clock_vector" />
<com.simplemobiletools.commons.views.MyTextView
android:id="@+id/scheduled_message_button"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="start"
android:layout_toStartOf="@+id/discard_scheduled_message"
android:layout_toEndOf="@+id/scheduled_message_icon"
android:background="@drawable/ripple_background"
android:clickable="true"
android:focusable="true"
android:gravity="center_vertical"
android:minHeight="@dimen/normal_icon_size"
android:paddingStart="@dimen/small_margin"
android:paddingEnd="@dimen/activity_margin"
android:textSize="@dimen/middle_text_size"
tools:ignore="HardcodedText"
tools:text="Tomorrow at 6PM GMT +05:30" />
<ImageView
android:id="@+id/discard_scheduled_message"
android:layout_width="@dimen/normal_icon_size"
android:layout_height="@dimen/normal_icon_size"
android:layout_alignParentEnd="true"
android:layout_gravity="end"
android:background="?selectableItemBackgroundBorderless"
android:contentDescription="@string/cancel_schedule_send"
android:padding="@dimen/normal_margin"
android:src="@drawable/ic_cross_vector" />
</RelativeLayout>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/thread_attachments_recyclerview"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/tiny_margin"
android:layout_marginBottom="@dimen/small_margin"
android:clipToPadding="false"
android:orientation="horizontal"
android:paddingStart="@dimen/normal_margin"
android:paddingEnd="@dimen/normal_margin"
android:scrollbars="none"
android:visibility="gone"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
app:layout_constraintBottom_toTopOf="@id/thread_type_message"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_goneMarginTop="@dimen/medium_margin"
tools:itemCount="2"
tools:listitem="@layout/item_attachment_document_preview"
app:layout_constraintTop_toBottomOf="@id/thread_messages_fastscroller"
tools:visibility="visible" />
<ImageView
android:id="@+id/thread_add_attachment"
android:layout_width="@dimen/normal_icon_size"
android:layout_height="@dimen/normal_icon_size"
android:layout_marginStart="@dimen/small_margin"
android:alpha="0.9"
android:background="?selectableItemBackgroundBorderless"
android:contentDescription="@string/attachment"
android:padding="@dimen/normal_margin"
android:src="@drawable/ic_plus_vector"
app:layout_constraintBottom_toBottomOf="@+id/thread_type_message"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@+id/thread_type_message" />
<com.simplemobiletools.commons.views.MyEditText
android:id="@+id/thread_type_message"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/small_margin"
android:layout_marginEnd="@dimen/small_margin"
android:background="@android:color/transparent"
android:gravity="center_vertical"
android:hint="@string/type_a_message"
android:inputType="textCapSentences|textMultiLine"
android:minHeight="@dimen/normal_icon_size"
app:layout_constraintBottom_toTopOf="@+id/attachment_picker_divider"
app:layout_constraintEnd_toStartOf="@id/thread_select_sim_icon"
app:layout_constraintStart_toEndOf="@+id/thread_add_attachment" />
<ImageView
android:id="@+id/thread_select_sim_icon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:alpha="0.9"
android:background="?attr/selectableItemBackgroundBorderless"
android:paddingStart="@dimen/medium_margin"
android:paddingTop="@dimen/normal_margin"
android:paddingEnd="@dimen/medium_margin"
android:paddingBottom="@dimen/normal_margin"
android:src="@drawable/ic_sim_vector"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="@+id/thread_type_message"
app:layout_constraintEnd_toStartOf="@id/thread_character_counter"
app:layout_constraintTop_toTopOf="@+id/thread_type_message"
tools:visibility="visible" />
<TextView
android:id="@+id/thread_select_sim_number"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:textSize="@dimen/normal_text_size"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="@id/thread_select_sim_icon"
app:layout_constraintEnd_toEndOf="@id/thread_select_sim_icon"
app:layout_constraintStart_toStartOf="@id/thread_select_sim_icon"
app:layout_constraintTop_toTopOf="@id/thread_select_sim_icon"
tools:text="1"
tools:textColor="@color/dark_grey"
tools:visibility="visible" />
<com.simplemobiletools.commons.views.MyTextView
android:id="@+id/thread_character_counter"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:paddingStart="@dimen/small_margin"
android:paddingEnd="@dimen/small_margin"
android:text="0"
android:textSize="@dimen/normal_text_size"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="@+id/thread_type_message"
app:layout_constraintEnd_toStartOf="@+id/thread_send_message"
app:layout_constraintTop_toTopOf="@+id/thread_type_message"
tools:ignore="HardcodedText"
tools:visibility="visible" />
<com.simplemobiletools.commons.views.MyButton
android:id="@+id/thread_send_message"
android:layout_width="@dimen/normal_icon_size"
android:layout_height="@dimen/normal_icon_size"
android:layout_marginEnd="@dimen/small_margin"
android:alpha="0.4"
android:background="?selectableItemBackgroundBorderless"
android:clickable="false"
android:contentDescription="@string/ok"
android:drawableTop="@drawable/ic_send_vector"
android:paddingVertical="@dimen/small_margin"
android:text="@string/sms"
android:textSize="@dimen/smaller_text_size"
app:layout_constraintBottom_toBottomOf="@+id/thread_type_message"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@+id/thread_type_message" />
<View
android:id="@+id/attachment_picker_divider"
android:layout_width="match_parent"
android:layout_height="@dimen/divider_height"
android:layout_alignParentStart="true"
android:layout_alignParentEnd="true"
android:background="@color/divider_grey"
android:importantForAccessibility="no"
app:layout_constraintBottom_toTopOf="@id/attachment_picker_holder" />
<ScrollView
android:id="@+id/attachment_picker_holder"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="@dimen/small_margin"
android:overScrollMode="always"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
tools:visibility="visible">
<include
layout="@layout/layout_attachment_picker"
layout="@layout/layout_thread_send_message_holder"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</ScrollView>
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<com.simplemobiletools.commons.views.MyTextView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/dialog_invalid_number_desc"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingStart="@dimen/big_margin"
android:paddingTop="@dimen/big_margin"
android:paddingEnd="@dimen/big_margin"
android:textIsSelectable="true"
tools:text="My sample text" />

View File

@ -0,0 +1,35 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/reply_disabled_info_holder"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:paddingHorizontal="@dimen/normal_margin">
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/reply_disabled_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:layout_gravity="center"
android:layout_toEndOf="@+id/reply_disabled_info"
android:clickable="true"
android:focusable="true"
android:padding="@dimen/activity_margin"
android:text="@string/invalid_short_code" />
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/reply_disabled_info"
android:layout_width="@dimen/normal_icon_size"
android:layout_height="@dimen/normal_icon_size"
android:layout_alignTop="@id/reply_disabled_text"
android:layout_alignBottom="@id/reply_disabled_text"
android:layout_alignParentEnd="true"
android:layout_gravity="end"
android:background="?selectableItemBackgroundBorderless"
android:contentDescription="@string/more_info"
android:padding="@dimen/normal_margin"
android:src="@drawable/ic_info_vector"
android:tooltipText="@string/more_info" />
</RelativeLayout>

View File

@ -0,0 +1,209 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/thread_send_message_holder"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<RelativeLayout
android:id="@+id/scheduled_message_holder"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/normal_margin"
android:layout_marginTop="@dimen/medium_margin"
android:layout_marginEnd="@dimen/medium_margin"
android:background="@drawable/section_holder_stroke"
android:orientation="horizontal"
android:visibility="gone"
app:layout_constraintBottom_toTopOf="@id/thread_attachments_recyclerview"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_goneMarginBottom="@dimen/medium_margin"
tools:visibility="visible">
<ImageView
android:id="@+id/scheduled_message_icon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignTop="@+id/scheduled_message_button"
android:layout_alignBottom="@+id/scheduled_message_button"
android:paddingTop="@dimen/medium_margin"
android:paddingBottom="@dimen/medium_margin"
android:src="@drawable/ic_clock_vector" />
<com.simplemobiletools.commons.views.MyTextView
android:id="@+id/scheduled_message_button"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="start"
android:layout_toStartOf="@+id/discard_scheduled_message"
android:layout_toEndOf="@+id/scheduled_message_icon"
android:background="@drawable/ripple_background"
android:clickable="true"
android:focusable="true"
android:gravity="center_vertical"
android:minHeight="@dimen/normal_icon_size"
android:paddingStart="@dimen/small_margin"
android:paddingEnd="@dimen/activity_margin"
android:textSize="@dimen/middle_text_size"
tools:ignore="HardcodedText"
tools:text="Tomorrow at 6PM GMT +05:30" />
<ImageView
android:id="@+id/discard_scheduled_message"
android:layout_width="@dimen/normal_icon_size"
android:layout_height="@dimen/normal_icon_size"
android:layout_alignParentEnd="true"
android:layout_gravity="end"
android:background="?selectableItemBackgroundBorderless"
android:contentDescription="@string/cancel_schedule_send"
android:padding="@dimen/normal_margin"
android:src="@drawable/ic_cross_vector" />
</RelativeLayout>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/thread_attachments_recyclerview"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/tiny_margin"
android:layout_marginBottom="@dimen/small_margin"
android:clipToPadding="false"
android:orientation="horizontal"
android:paddingStart="@dimen/normal_margin"
android:paddingEnd="@dimen/normal_margin"
android:scrollbars="none"
android:visibility="gone"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
app:layout_constraintBottom_toTopOf="@id/thread_type_message"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_goneMarginTop="@dimen/medium_margin"
tools:itemCount="2"
tools:listitem="@layout/item_attachment_document_preview"
tools:visibility="visible" />
<ImageView
android:id="@+id/thread_add_attachment"
android:layout_width="@dimen/normal_icon_size"
android:layout_height="@dimen/normal_icon_size"
android:layout_marginStart="@dimen/small_margin"
android:alpha="0.9"
android:background="?selectableItemBackgroundBorderless"
android:contentDescription="@string/attachment"
android:padding="@dimen/normal_margin"
android:src="@drawable/ic_plus_vector"
app:layout_constraintBottom_toBottomOf="@+id/thread_type_message"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@+id/thread_type_message" />
<com.simplemobiletools.commons.views.MyEditText
android:id="@+id/thread_type_message"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/small_margin"
android:layout_marginEnd="@dimen/small_margin"
android:background="@android:color/transparent"
android:gravity="center_vertical"
android:hint="@string/type_a_message"
android:inputType="textCapSentences|textMultiLine"
android:minHeight="@dimen/normal_icon_size"
app:layout_constraintBottom_toTopOf="@+id/attachment_picker_divider"
app:layout_constraintEnd_toStartOf="@id/thread_select_sim_icon"
app:layout_constraintStart_toEndOf="@+id/thread_add_attachment" />
<ImageView
android:id="@+id/thread_select_sim_icon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:alpha="0.9"
android:background="?attr/selectableItemBackgroundBorderless"
android:paddingStart="@dimen/medium_margin"
android:paddingTop="@dimen/normal_margin"
android:paddingEnd="@dimen/medium_margin"
android:paddingBottom="@dimen/normal_margin"
android:src="@drawable/ic_sim_vector"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="@+id/thread_type_message"
app:layout_constraintEnd_toStartOf="@id/thread_character_counter"
app:layout_constraintTop_toTopOf="@+id/thread_type_message"
tools:visibility="visible" />
<TextView
android:id="@+id/thread_select_sim_number"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:textSize="@dimen/normal_text_size"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="@id/thread_select_sim_icon"
app:layout_constraintEnd_toEndOf="@id/thread_select_sim_icon"
app:layout_constraintStart_toStartOf="@id/thread_select_sim_icon"
app:layout_constraintTop_toTopOf="@id/thread_select_sim_icon"
tools:text="1"
tools:textColor="@color/dark_grey"
tools:visibility="visible" />
<com.simplemobiletools.commons.views.MyTextView
android:id="@+id/thread_character_counter"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:paddingStart="@dimen/small_margin"
android:paddingEnd="@dimen/small_margin"
android:text="0"
android:textSize="@dimen/normal_text_size"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="@+id/thread_type_message"
app:layout_constraintEnd_toStartOf="@+id/thread_send_message"
app:layout_constraintTop_toTopOf="@+id/thread_type_message"
tools:ignore="HardcodedText"
tools:visibility="visible" />
<com.simplemobiletools.commons.views.MyButton
android:id="@+id/thread_send_message"
android:layout_width="@dimen/normal_icon_size"
android:layout_height="@dimen/normal_icon_size"
android:layout_marginEnd="@dimen/small_margin"
android:alpha="0.4"
android:background="?selectableItemBackgroundBorderless"
android:clickable="false"
android:contentDescription="@string/ok"
android:drawableTop="@drawable/ic_send_vector"
android:paddingVertical="@dimen/small_margin"
android:text="@string/sms"
android:textSize="@dimen/smaller_text_size"
app:layout_constraintBottom_toBottomOf="@+id/thread_type_message"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@+id/thread_type_message" />
<View
android:id="@+id/attachment_picker_divider"
android:layout_width="match_parent"
android:layout_height="@dimen/divider_height"
android:layout_alignParentStart="true"
android:layout_alignParentEnd="true"
android:background="@color/divider_grey"
android:importantForAccessibility="no"
app:layout_constraintBottom_toTopOf="@id/attachment_picker_holder" />
<ScrollView
android:id="@+id/attachment_picker_holder"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="@dimen/small_margin"
android:overScrollMode="always"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
tools:visibility="visible">
<include
layout="@layout/layout_attachment_picker"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</ScrollView>
</androidx.constraintlayout.widget.ConstraintLayout>