diff --git a/app/src/main/kotlin/com/simplemobiletools/smsmessenger/activities/ThreadActivity.kt b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/activities/ThreadActivity.kt index a0ae324c..bb1f8504 100644 --- a/app/src/main/kotlin/com/simplemobiletools/smsmessenger/activities/ThreadActivity.kt +++ b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/activities/ThreadActivity.kt @@ -55,6 +55,10 @@ 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.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.ThreadItem.* import kotlinx.android.synthetic.main.activity_thread.* @@ -1167,7 +1171,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 +1351,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 } diff --git a/app/src/main/kotlin/com/simplemobiletools/smsmessenger/extensions/Context.kt b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/extensions/Context.kt index 7af9e021..6ed5c9ea 100644 --- a/app/src/main/kotlin/com/simplemobiletools/smsmessenger/extensions/Context.kt +++ b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/extensions/Context.kt @@ -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) { val unreadCount = conversations.count { !it.read } if (unreadCount == 0) { diff --git a/app/src/main/kotlin/com/simplemobiletools/smsmessenger/helpers/Constants.kt b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/helpers/Constants.kt index 74b72a02..f89d86f5 100644 --- a/app/src/main/kotlin/com/simplemobiletools/smsmessenger/helpers/Constants.kt +++ b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/helpers/Constants.kt @@ -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() +} diff --git a/app/src/main/kotlin/com/simplemobiletools/smsmessenger/helpers/Messaging.kt b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/helpers/Messaging.kt deleted file mode 100644 index 81199e2e..00000000 --- a/app/src/main/kotlin/com/simplemobiletools/smsmessenger/helpers/Messaging.kt +++ /dev/null @@ -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, subscriptionId: Int?, attachments: List) { - 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() -} diff --git a/app/src/main/kotlin/com/simplemobiletools/smsmessenger/messaging/Messaging.kt b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/messaging/Messaging.kt new file mode 100644 index 00000000..fecd44ce --- /dev/null +++ b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/messaging/Messaging.kt @@ -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, subId: Int?, attachments: List) { + 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) + } + } +} + diff --git a/app/src/main/kotlin/com/simplemobiletools/smsmessenger/messaging/MessagingUtils.kt b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/messaging/MessagingUtils.kt new file mode 100644 index 00000000..ec845d6d --- /dev/null +++ b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/messaging/MessagingUtils.kt @@ -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, 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, attachments: List, 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 = "|" + } +} diff --git a/app/src/main/kotlin/com/simplemobiletools/smsmessenger/messaging/ScheduledMessage.kt b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/messaging/ScheduledMessage.kt new file mode 100644 index 00000000..9972dc3b --- /dev/null +++ b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/messaging/ScheduledMessage.kt @@ -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() +} diff --git a/app/src/main/kotlin/com/simplemobiletools/smsmessenger/messaging/SendStatusReceiver.kt b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/messaging/SendStatusReceiver.kt new file mode 100644 index 00000000..41ff169a --- /dev/null +++ b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/messaging/SendStatusReceiver.kt @@ -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 + } +} diff --git a/app/src/main/kotlin/com/simplemobiletools/smsmessenger/messaging/SmsException.kt b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/messaging/SmsException.kt new file mode 100644 index 00000000..96406915 --- /dev/null +++ b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/messaging/SmsException.kt @@ -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 + } +} diff --git a/app/src/main/kotlin/com/simplemobiletools/smsmessenger/messaging/SmsManager.kt b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/messaging/SmsManager.kt new file mode 100644 index 00000000..4f1b4d03 --- /dev/null +++ b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/messaging/SmsManager.kt @@ -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!! +} diff --git a/app/src/main/kotlin/com/simplemobiletools/smsmessenger/messaging/SmsSender.kt b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/messaging/SmsSender.kt new file mode 100644 index 00000000..843ed05a --- /dev/null +++ b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/messaging/SmsSender.kt @@ -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, serviceCenter: String?, + requireDeliveryReport: Boolean, messageUri: Uri + ) { + val smsManager = getSmsManager(subId) + val messageCount = messages.size + val deliveryIntents = ArrayList(messageCount) + val sentIntents = ArrayList(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!! + } + } +} diff --git a/app/src/main/kotlin/com/simplemobiletools/smsmessenger/receivers/DirectReplyReceiver.kt b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/receivers/DirectReplyReceiver.kt index 496e0a63..94365420 100644 --- a/app/src/main/kotlin/com/simplemobiletools/smsmessenger/receivers/DirectReplyReceiver.kt +++ b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/receivers/DirectReplyReceiver.kt @@ -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) diff --git a/app/src/main/kotlin/com/simplemobiletools/smsmessenger/receivers/MmsSentReceiver.kt b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/receivers/MmsSentReceiver.kt index c7ac0274..d4553e8a 100644 --- a/app/src/main/kotlin/com/simplemobiletools/smsmessenger/receivers/MmsSentReceiver.kt +++ b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/receivers/MmsSentReceiver.kt @@ -1,15 +1,44 @@ 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 com.simplemobiletools.commons.extensions.showErrorToast import com.simplemobiletools.smsmessenger.helpers.refreshMessages +import com.simplemobiletools.smsmessenger.messaging.SendStatusReceiver +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 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) { refreshMessages() } } + + companion object { + private const val EXTRA_CONTENT_URI = "content_uri" + private const val EXTRA_FILE_PATH = "file_path" + } } diff --git a/app/src/main/kotlin/com/simplemobiletools/smsmessenger/receivers/ScheduledMessageReceiver.kt b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/receivers/ScheduledMessageReceiver.kt index 1b32ca2c..13cb8c86 100644 --- a/app/src/main/kotlin/com/simplemobiletools/smsmessenger/receivers/ScheduledMessageReceiver.kt +++ b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/receivers/ScheduledMessageReceiver.kt @@ -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 diff --git a/app/src/main/kotlin/com/simplemobiletools/smsmessenger/receivers/SmsStatusDeliveredReceiver.kt b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/receivers/SmsStatusDeliveredReceiver.kt index 97b1f5ad..45542cb3 100644 --- a/app/src/main/kotlin/com/simplemobiletools/smsmessenger/receivers/SmsStatusDeliveredReceiver.kt +++ b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/receivers/SmsStatusDeliveredReceiver.kt @@ -1,26 +1,90 @@ package com.simplemobiletools.smsmessenger.receivers +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.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.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) { - 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 uri: Uri? = intent.data + val resultCode = resultCode + + 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 { - val status = Telephony.Sms.STATUS_COMPLETE - context.updateMessageStatus(messageId, status) + val status = Sms.Sent.STATUS_COMPLETE val updated = context.messagesDB.updateStatus(messageId, status) if (updated == 0) { Handler(Looper.getMainLooper()).postDelayed({ diff --git a/app/src/main/kotlin/com/simplemobiletools/smsmessenger/receivers/SmsStatusSentReceiver.kt b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/receivers/SmsStatusSentReceiver.kt index 3b324d63..b8d74fa8 100644 --- a/app/src/main/kotlin/com/simplemobiletools/smsmessenger/receivers/SmsStatusSentReceiver.kt +++ b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/receivers/SmsStatusSentReceiver.kt @@ -1,35 +1,98 @@ package com.simplemobiletools.smsmessenger.receivers +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.os.Handler 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.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 +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) { - 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 + + 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 { 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({