diff --git a/app/src/main/kotlin/com/simplemobiletools/smsmessenger/activities/NewConversationActivity.kt b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/activities/NewConversationActivity.kt index e0b0dd48..d1214e94 100644 --- a/app/src/main/kotlin/com/simplemobiletools/smsmessenger/activities/NewConversationActivity.kt +++ b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/activities/NewConversationActivity.kt @@ -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) } 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..dfc044ee 100644 --- a/app/src/main/kotlin/com/simplemobiletools/smsmessenger/activities/ThreadActivity.kt +++ b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/activities/ThreadActivity.kt @@ -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 } diff --git a/app/src/main/kotlin/com/simplemobiletools/smsmessenger/adapters/ConversationsAdapter.kt b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/adapters/ConversationsAdapter.kt index 6fb292d9..4b9d219d 100644 --- a/app/src/main/kotlin/com/simplemobiletools/smsmessenger/adapters/ConversationsAdapter.kt +++ b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/adapters/ConversationsAdapter.kt @@ -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) diff --git a/app/src/main/kotlin/com/simplemobiletools/smsmessenger/dialogs/InvalidNumberDialog.kt b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/dialogs/InvalidNumberDialog.kt new file mode 100644 index 00000000..ee49c311 --- /dev/null +++ b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/dialogs/InvalidNumberDialog.kt @@ -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) + } + } +} 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/helpers/NotificationHelper.kt b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/helpers/NotificationHelper.kt index 827cc804..35117def 100644 --- a/app/src/main/kotlin/com/simplemobiletools/smsmessenger/helpers/NotificationHelper.kt +++ b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/helpers/NotificationHelper.kt @@ -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) 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..5a92c719 --- /dev/null +++ b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/messaging/Messaging.kt @@ -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, 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( + 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() } +} 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..cc52bf2c --- /dev/null +++ b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/messaging/MessagingUtils.kt @@ -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, 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, 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)) + } + } + } + + 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 = "|" + } +} 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/SmsException.kt b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/messaging/SmsException.kt new file mode 100644 index 00000000..e4bc14b0 --- /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 = -1 + const val ERROR_PERSISTING_MESSAGE = -2 + const val ERROR_SENDING_MESSAGE = -3 + } +} 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..f32f81e6 --- /dev/null +++ b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/messaging/SmsSender.kt @@ -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, serviceCenter: String?, + requireDeliveryReport: Boolean, messageUri: Uri + ) { + val smsManager = getSmsManager(subId) + val messageCount = messages.size + val deliveryIntents = ArrayList(messageCount) + val sentIntents = ArrayList(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!! + } + } +} 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..d9ea5b18 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,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" + } } 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/SendStatusReceiver.kt b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/receivers/SendStatusReceiver.kt new file mode 100644 index 00000000..73c91bf0 --- /dev/null +++ b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/receivers/SendStatusReceiver.kt @@ -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 + } +} 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..69b92315 100644 --- a/app/src/main/kotlin/com/simplemobiletools/smsmessenger/receivers/SmsStatusDeliveredReceiver.kt +++ b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/receivers/SmsStatusDeliveredReceiver.kt @@ -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({ - ensureBackgroundThread { - context.messagesDB.updateStatus(messageId, status) + 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 } - }, 2000) + } + 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) + } refreshMessages() } } 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..db7f677f 100644 --- a/app/src/main/kotlin/com/simplemobiletools/smsmessenger/receivers/SmsStatusSentReceiver.kt +++ b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/receivers/SmsStatusSentReceiver.kt @@ -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 - } - - 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) + Sms.MESSAGE_TYPE_FAILED } + context.messagesDB.updateType(messageId, type) refreshMessages() } } diff --git a/app/src/main/kotlin/com/simplemobiletools/smsmessenger/services/HeadlessSmsSendService.kt b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/services/HeadlessSmsSendService.kt index bdea1dbe..16b74522 100644 --- a/app/src/main/kotlin/com/simplemobiletools/smsmessenger/services/HeadlessSmsSendService.kt +++ b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/services/HeadlessSmsSendService.kt @@ -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) { } diff --git a/app/src/main/res/layout/activity_thread.xml b/app/src/main/res/layout/activity_thread.xml index 9329a50e..fcc3fb2d 100644 --- a/app/src/main/res/layout/activity_thread.xml +++ b/app/src/main/res/layout/activity_thread.xml @@ -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 @@ - + + + app:layout_constraintStart_toStartOf="parent" /> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/layout/dialog_invalid_number.xml b/app/src/main/res/layout/dialog_invalid_number.xml new file mode 100644 index 00000000..d8b208ea --- /dev/null +++ b/app/src/main/res/layout/dialog_invalid_number.xml @@ -0,0 +1,11 @@ + + diff --git a/app/src/main/res/layout/layout_invalid_short_code_info.xml b/app/src/main/res/layout/layout_invalid_short_code_info.xml new file mode 100644 index 00000000..2243f2a3 --- /dev/null +++ b/app/src/main/res/layout/layout_invalid_short_code_info.xml @@ -0,0 +1,35 @@ + + + + + + + + diff --git a/app/src/main/res/layout/layout_thread_send_message_holder.xml b/app/src/main/res/layout/layout_thread_send_message_holder.xml new file mode 100644 index 00000000..4138565a --- /dev/null +++ b/app/src/main/res/layout/layout_thread_send_message_holder.xml @@ -0,0 +1,209 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +