Rewrite, move SMS related code into the app

This commit is contained in:
Naveen
2023-01-03 15:43:14 +05:30
parent 7bce8ab31b
commit 1f36738be0
16 changed files with 647 additions and 162 deletions

View File

@ -55,6 +55,10 @@ import com.simplemobiletools.smsmessenger.dialogs.RenameConversationDialog
import com.simplemobiletools.smsmessenger.dialogs.ScheduleMessageDialog import com.simplemobiletools.smsmessenger.dialogs.ScheduleMessageDialog
import com.simplemobiletools.smsmessenger.extensions.* import com.simplemobiletools.smsmessenger.extensions.*
import com.simplemobiletools.smsmessenger.helpers.* import com.simplemobiletools.smsmessenger.helpers.*
import com.simplemobiletools.smsmessenger.messaging.cancelScheduleSendPendingIntent
import com.simplemobiletools.smsmessenger.messaging.isLongMmsMessage
import com.simplemobiletools.smsmessenger.messaging.scheduleMessage
import com.simplemobiletools.smsmessenger.messaging.sendMessageCompat
import com.simplemobiletools.smsmessenger.models.* import com.simplemobiletools.smsmessenger.models.*
import com.simplemobiletools.smsmessenger.models.ThreadItem.* import com.simplemobiletools.smsmessenger.models.ThreadItem.*
import kotlinx.android.synthetic.main.activity_thread.* import kotlinx.android.synthetic.main.activity_thread.*
@ -1167,7 +1171,7 @@ class ThreadActivity : SimpleActivity() {
try { try {
refreshedSinceSent = false refreshedSinceSent = false
sendMessage(text, addresses, subscriptionId, attachments) sendMessageCompat(text, addresses, subscriptionId, attachments)
ensureBackgroundThread { ensureBackgroundThread {
val messageIds = messages.map { it.id } val messageIds = messages.map { it.id }
val message = getMessages(threadId, getImageResolutions = true, limit = 1).firstOrNull { it.id !in messageIds } val message = getMessages(threadId, getImageResolutions = true, limit = 1).firstOrNull { it.id !in messageIds }
@ -1347,7 +1351,7 @@ class ThreadActivity : SimpleActivity() {
private fun isMmsMessage(text: String): Boolean { private fun isMmsMessage(text: String): Boolean {
val isGroupMms = participants.size > 1 && config.sendGroupMessageMMS val isGroupMms = participants.size > 1 && config.sendGroupMessageMMS
val isLongMmsMessage = isLongMmsMessage(text) && config.sendLongMessageMMS val isLongMmsMessage = isLongMmsMessage(text)
return getAttachmentSelections().isNotEmpty() || isGroupMms || isLongMmsMessage return getAttachmentSelections().isNotEmpty() || isGroupMms || isLongMmsMessage
} }

View File

@ -1,6 +1,7 @@
package com.simplemobiletools.smsmessenger.extensions package com.simplemobiletools.smsmessenger.extensions
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.Application
import android.content.ContentResolver import android.content.ContentResolver
import android.content.ContentValues import android.content.ContentValues
import android.content.Context import android.content.Context
@ -18,7 +19,6 @@ import android.text.TextUtils
import com.bumptech.glide.Glide import com.bumptech.glide.Glide
import com.bumptech.glide.load.engine.DiskCacheStrategy import com.bumptech.glide.load.engine.DiskCacheStrategy
import com.bumptech.glide.request.RequestOptions import com.bumptech.glide.request.RequestOptions
import com.klinker.android.send_message.Transaction.getAddressSeparator
import com.simplemobiletools.commons.extensions.* import com.simplemobiletools.commons.extensions.*
import com.simplemobiletools.commons.helpers.* import com.simplemobiletools.commons.helpers.*
import com.simplemobiletools.commons.models.PhoneNumber 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.ConversationsDao
import com.simplemobiletools.smsmessenger.interfaces.MessageAttachmentsDao import com.simplemobiletools.smsmessenger.interfaces.MessageAttachmentsDao
import com.simplemobiletools.smsmessenger.interfaces.MessagesDao 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 com.simplemobiletools.smsmessenger.models.*
import me.leolin.shortcutbadger.ShortcutBadger import me.leolin.shortcutbadger.ShortcutBadger
import java.io.FileNotFoundException import java.io.FileNotFoundException
@ -49,6 +52,10 @@ val Context.messagesDB: MessagesDao get() = getMessagesDB().MessagesDao()
val Context.notificationHelper get() = NotificationHelper(this) val Context.notificationHelper get() = NotificationHelper(this)
val Context.messagingUtils get() = MessagingUtils(this)
val Context.smsSender get() = SmsSender.getInstance(applicationContext as Application)
fun Context.getMessages( fun Context.getMessages(
threadId: Long, threadId: Long,
getImageResolutions: Boolean, getImageResolutions: Boolean,
@ -103,7 +110,7 @@ fun Context.getMessages(
val thread = cursor.getLongValue(Sms.THREAD_ID) val thread = cursor.getLongValue(Sms.THREAD_ID)
val subscriptionId = cursor.getIntValue(Sms.SUBSCRIPTION_ID) val subscriptionId = cursor.getIntValue(Sms.SUBSCRIPTION_ID)
val status = cursor.getIntValue(Sms.STATUS) 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 phoneNumber = PhoneNumber(number, 0, "", number)
val participantPhoto = getNameAndPhotoFromPhoneNumber(number) val participantPhoto = getNameAndPhotoFromPhoneNumber(number)
SimpleContact(0, 0, participantPhoto.name, photoUri, arrayListOf(phoneNumber), ArrayList(), ArrayList()) 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>) { fun Context.updateUnreadCountBadge(conversations: List<Conversation>) {
val unreadCount = conversations.count { !it.read } val unreadCount = conversations.count { !it.read }
if (unreadCount == 0) { if (unreadCount == 0) {

View File

@ -2,6 +2,10 @@ package com.simplemobiletools.smsmessenger.helpers
import com.simplemobiletools.smsmessenger.models.Events import com.simplemobiletools.smsmessenger.models.Events
import org.greenrobot.eventbus.EventBus 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_ID = "thread_id"
const val THREAD_TITLE = "thread_title" const val THREAD_TITLE = "thread_title"
@ -81,3 +85,10 @@ const val PICK_CONTACT_INTENT = 48
fun refreshMessages() { fun refreshMessages() {
EventBus.getDefault().post(Events.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

@ -0,0 +1,58 @@
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(id = R.string.unknown_error_occurred, length = LENGTH_LONG)
}
} catch (e: Exception) {
showErrorToast(e)
}
}
}

View File

@ -0,0 +1,147 @@
package com.simplemobiletools.smsmessenger.messaging
import android.app.Activity
import android.content.ContentValues
import android.content.Context
import android.net.Uri
import android.provider.Telephony.Sms
import android.telephony.SmsManager
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
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
)
context.smsSender.sendMessage(
subId = subId, destination = address, body = text, serviceCenter = null,
requireDeliveryReport = requireDeliveryReport, messageUri = messageUri
)
}
}
@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))
}
}
}
try {
transaction.sendNewMessage(message)
} catch (e: Exception) {
context.showErrorToast(e)
}
}
fun maybeShowErrorToast(resultCode: Int, errorCode: Int) {
if (resultCode != Activity.RESULT_OK) {
val msgId = 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 = msgId, 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,33 @@
package com.simplemobiletools.smsmessenger.messaging
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 MESSAGE_SENT_ACTION = "com.simplemobiletools.smsmessenger.receiver.SendStatusReceiver.MESSAGE_SENT"
const val MESSAGE_DELIVERED_ACTION = "com.simplemobiletools.smsmessenger.receiver.SendStatusReceiver.MESSAGE_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

@ -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 = 0
const val ERROR_PERSISTING_MESSAGE = 1
const val ERROR_SENDING_MESSAGE = 2
}
}

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,128 @@
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.smsmessenger.messaging.SmsException.Companion.EMPTY_DESTINATION_ADDRESS
import com.simplemobiletools.smsmessenger.messaging.SmsException.Companion.ERROR_SENDING_MESSAGE
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)
val flags = PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
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.MESSAGE_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.MESSAGE_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.REPLY
import com.simplemobiletools.smsmessenger.helpers.THREAD_ID import com.simplemobiletools.smsmessenger.helpers.THREAD_ID
import com.simplemobiletools.smsmessenger.helpers.THREAD_NUMBER import com.simplemobiletools.smsmessenger.helpers.THREAD_NUMBER
import com.simplemobiletools.smsmessenger.helpers.sendMessage import com.simplemobiletools.smsmessenger.messaging.sendMessageCompat
class DirectReplyReceiver : BroadcastReceiver() { class DirectReplyReceiver : BroadcastReceiver() {
@SuppressLint("MissingPermission") @SuppressLint("MissingPermission")
@ -38,7 +38,7 @@ class DirectReplyReceiver : BroadcastReceiver() {
ensureBackgroundThread { ensureBackgroundThread {
try { 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() val message = context.getMessages(threadId, getImageResolutions = false, includeScheduledMessages = false, limit = 1).lastOrNull()
if (message != null) { if (message != null) {
context.messagesDB.insertOrUpdate(message) context.messagesDB.insertOrUpdate(message)

View File

@ -1,15 +1,44 @@
package com.simplemobiletools.smsmessenger.receivers package com.simplemobiletools.smsmessenger.receivers
import android.app.Activity import android.app.Activity
import android.content.ContentValues
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.database.sqlite.SQLiteException
import android.net.Uri
import android.provider.Telephony
import com.simplemobiletools.commons.extensions.showErrorToast
import com.simplemobiletools.smsmessenger.helpers.refreshMessages import com.simplemobiletools.smsmessenger.helpers.refreshMessages
import com.simplemobiletools.smsmessenger.messaging.SendStatusReceiver
import java.io.File
class MmsSentReceiver : com.klinker.android.send_message.MmsSentReceiver() { /** Handles updating databases and states when a MMS message is sent. */
override fun onMessageStatusUpdated(context: Context?, intent: Intent?, resultCode: Int) { class MmsSentReceiver : SendStatusReceiver() {
super.onMessageStatusUpdated(context, intent, resultCode)
override fun updateAndroidDatabase(context: Context, intent: Intent, receiverResultCode: Int) {
val uri = Uri.parse(intent.getStringExtra(EXTRA_CONTENT_URI))
val values = ContentValues(1)
values.put(Telephony.Mms.MESSAGE_BOX, Telephony.Mms.MESSAGE_BOX_SENT)
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) { if (resultCode == Activity.RESULT_OK) {
refreshMessages() 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.SCHEDULED_MESSAGE_ID
import com.simplemobiletools.smsmessenger.helpers.THREAD_ID import com.simplemobiletools.smsmessenger.helpers.THREAD_ID
import com.simplemobiletools.smsmessenger.helpers.refreshMessages import com.simplemobiletools.smsmessenger.helpers.refreshMessages
import com.simplemobiletools.smsmessenger.helpers.sendMessage import com.simplemobiletools.smsmessenger.messaging.sendMessageCompat
class ScheduledMessageReceiver : BroadcastReceiver() { class ScheduledMessageReceiver : BroadcastReceiver() {
@ -46,7 +46,7 @@ class ScheduledMessageReceiver : BroadcastReceiver() {
try { try {
Handler(Looper.getMainLooper()).post { 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 // delete temporary conversation and message as it's already persisted to the telephony db now

View File

@ -1,26 +1,90 @@
package com.simplemobiletools.smsmessenger.receivers package com.simplemobiletools.smsmessenger.receivers
import android.annotation.SuppressLint
import android.app.Activity
import android.content.ContentValues
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.net.Uri import android.net.Uri
import android.os.Handler import android.os.Handler
import android.os.Looper import android.os.Looper
import android.provider.Telephony import android.provider.Telephony.Sms
import com.klinker.android.send_message.DeliveredReceiver
import com.simplemobiletools.commons.helpers.ensureBackgroundThread import com.simplemobiletools.commons.helpers.ensureBackgroundThread
import com.simplemobiletools.smsmessenger.extensions.messagesDB import com.simplemobiletools.smsmessenger.extensions.messagesDB
import com.simplemobiletools.smsmessenger.extensions.updateMessageStatus
import com.simplemobiletools.smsmessenger.helpers.refreshMessages import com.simplemobiletools.smsmessenger.helpers.refreshMessages
import com.simplemobiletools.smsmessenger.messaging.SendStatusReceiver
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) { override fun updateAndroidDatabase(context: Context, intent: Intent, receiverResultCode: Int) {
if (intent.extras?.containsKey("message_uri") == true) { val uri: Uri? = intent.data
val uri = Uri.parse(intent.getStringExtra("message_uri")) val resultCode = resultCode
val messageId = uri?.lastPathSegment?.toLong() ?: 0L
try {
when (resultCode) {
Activity.RESULT_OK -> {
if (uri != null) {
val values = ContentValues().apply {
put(Sms.Sent.STATUS, Sms.Sent.STATUS_COMPLETE)
put(Sms.Sent.DATE_SENT, System.currentTimeMillis())
put(Sms.Sent.READ, true)
}
context.contentResolver.update(uri, values, null, null)
} else {
updateLatestSmsStatus(context, status = Sms.Sent.STATUS_COMPLETE, read = true, date = System.currentTimeMillis())
}
}
Activity.RESULT_CANCELED -> {
if (uri != null) {
val values = ContentValues().apply {
put(Sms.Sent.STATUS, Sms.Sent.STATUS_FAILED)
put(Sms.Sent.DATE_SENT, System.currentTimeMillis())
put(Sms.Sent.READ, true)
put(Sms.Sent.ERROR_CODE, resultCode)
}
context.contentResolver.update(uri, values, null, null)
} else {
updateLatestSmsStatus(context, status = Sms.Sent.STATUS_FAILED, read = true, errorCode = resultCode)
}
}
}
} catch (e: Exception) {
e.printStackTrace()
}
}
@SuppressLint("Range")
private fun updateLatestSmsStatus(context: Context, status: Int, read: Boolean, date: Long = -1L, errorCode: Int = -1) {
val query = context.contentResolver.query(Sms.Sent.CONTENT_URI, null, null, null, "date desc")
// mark message as delivered in database
if (query!!.moveToFirst()) {
val id = query.getString(query.getColumnIndex(Sms.Sent._ID))
val values = ContentValues().apply {
put(Sms.Sent.STATUS, status)
put(Sms.Sent.READ, read)
if (date != -1L) {
put(Sms.Sent.DATE_SENT, date)
}
if (errorCode != -1) {
put(Sms.Sent.ERROR_CODE, errorCode)
}
}
context.contentResolver.update(Sms.Sent.CONTENT_URI, values, "_id=$id", null)
}
query.close()
}
override fun updateAppDatabase(context: Context, intent: Intent, receiverResultCode: Int) {
val uri = intent.data
if (uri != null) {
val messageId = uri.lastPathSegment?.toLong() ?: 0L
ensureBackgroundThread { ensureBackgroundThread {
val status = Telephony.Sms.STATUS_COMPLETE val status = Sms.Sent.STATUS_COMPLETE
context.updateMessageStatus(messageId, status)
val updated = context.messagesDB.updateStatus(messageId, status) val updated = context.messagesDB.updateStatus(messageId, status)
if (updated == 0) { if (updated == 0) {
Handler(Looper.getMainLooper()).postDelayed({ Handler(Looper.getMainLooper()).postDelayed({

View File

@ -1,35 +1,98 @@
package com.simplemobiletools.smsmessenger.receivers package com.simplemobiletools.smsmessenger.receivers
import android.annotation.SuppressLint
import android.app.Activity import android.app.Activity
import android.content.ContentValues
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.net.Uri import android.net.Uri
import android.os.Handler import android.os.Handler
import android.os.Looper import android.os.Looper
import android.provider.Telephony import android.provider.Telephony.Sms
import android.telephony.SmsManager.RESULT_ERROR_GENERIC_FAILURE
import android.telephony.SmsManager.RESULT_ERROR_NO_SERVICE
import android.telephony.SmsManager.RESULT_ERROR_NULL_PDU
import android.telephony.SmsManager.RESULT_ERROR_RADIO_OFF
import androidx.lifecycle.Lifecycle import androidx.lifecycle.Lifecycle
import androidx.lifecycle.ProcessLifecycleOwner import androidx.lifecycle.ProcessLifecycleOwner
import com.klinker.android.send_message.SentReceiver
import com.simplemobiletools.commons.extensions.getMyContactsCursor import com.simplemobiletools.commons.extensions.getMyContactsCursor
import com.simplemobiletools.commons.helpers.ensureBackgroundThread import com.simplemobiletools.commons.helpers.ensureBackgroundThread
import com.simplemobiletools.smsmessenger.extensions.* import com.simplemobiletools.smsmessenger.extensions.*
import com.simplemobiletools.smsmessenger.helpers.refreshMessages import com.simplemobiletools.smsmessenger.helpers.refreshMessages
import com.simplemobiletools.smsmessenger.messaging.SendStatusReceiver
class SmsStatusSentReceiver : SentReceiver() { /** Handles updating databases and states when a SMS message is sent. */
@SuppressLint("Range")
class SmsStatusSentReceiver : SendStatusReceiver() {
override fun onMessageStatusUpdated(context: Context, intent: Intent, receiverResultCode: Int) { override fun updateAndroidDatabase(context: Context, intent: Intent, receiverResultCode: Int) {
if (intent.extras?.containsKey("message_uri") == true) { val messageUri: Uri? = intent.data
val uri = Uri.parse(intent.getStringExtra("message_uri")) val resultCode = resultCode
val messageId = uri?.lastPathSegment?.toLong() ?: 0L
try {
when (resultCode) {
Activity.RESULT_OK -> if (messageUri != null) {
try {
val values = ContentValues()
values.put(Sms.Outbox.TYPE, Sms.MESSAGE_TYPE_SENT)
values.put(Sms.Outbox.READ, 1)
context.contentResolver.update(messageUri, values, null, null)
} catch (e: NullPointerException) {
updateLatestSms(context = context, type = Sms.MESSAGE_TYPE_SENT, read = 1)
}
} else {
updateLatestSms(context = context, type = Sms.MESSAGE_TYPE_FAILED, read = 1)
}
RESULT_ERROR_GENERIC_FAILURE, RESULT_ERROR_NO_SERVICE, RESULT_ERROR_NULL_PDU, RESULT_ERROR_RADIO_OFF -> {
if (messageUri != null) {
val values = ContentValues()
values.put(Sms.Outbox.TYPE, Sms.MESSAGE_TYPE_FAILED)
values.put(Sms.Outbox.READ, true)
values.put(Sms.Outbox.ERROR_CODE, resultCode)
context.contentResolver.update(messageUri, values, null, null)
} else {
updateLatestSms(context = context, type = Sms.MESSAGE_TYPE_FAILED, read = 1, resultCode = resultCode)
}
}
}
} catch (e: Exception) {
e.printStackTrace()
}
context.messagingUtils.maybeShowErrorToast(
resultCode = resultCode,
errorCode = intent.getIntExtra(EXTRA_ERROR_CODE, NO_ERROR_CODE)
)
}
private fun updateLatestSms(context: Context, type: Int, read: Int, resultCode: Int = -1) {
val query = context.contentResolver.query(Sms.Outbox.CONTENT_URI, null, null, null, null)
if (query != null && query.moveToFirst()) {
val id = query.getString(query.getColumnIndex(Sms.Outbox._ID))
val values = ContentValues()
values.put(Sms.Outbox.TYPE, type)
values.put(Sms.Outbox.READ, read)
if (resultCode != -1) {
values.put(Sms.Outbox.ERROR_CODE, resultCode)
}
context.contentResolver.update(Sms.Outbox.CONTENT_URI, values, "_id=$id", null)
query.close()
}
}
override fun updateAppDatabase(context: Context, intent: Intent, receiverResultCode: Int) {
val uri = intent.data
if (uri != null) {
val messageId = uri.lastPathSegment?.toLong() ?: 0L
ensureBackgroundThread { ensureBackgroundThread {
val type = if (receiverResultCode == Activity.RESULT_OK) { val type = if (receiverResultCode == Activity.RESULT_OK) {
Telephony.Sms.MESSAGE_TYPE_SENT Sms.MESSAGE_TYPE_SENT
} else { } else {
showSendingFailedNotification(context, messageId) showSendingFailedNotification(context, messageId)
Telephony.Sms.MESSAGE_TYPE_FAILED Sms.MESSAGE_TYPE_FAILED
} }
context.updateMessageType(messageId, type)
val updated = context.messagesDB.updateType(messageId, type) val updated = context.messagesDB.updateType(messageId, type)
if (updated == 0) { if (updated == 0) {
Handler(Looper.getMainLooper()).postDelayed({ Handler(Looper.getMainLooper()).postDelayed({