Rename package to org.fossify.messages

This commit is contained in:
Naveen 2024-01-18 01:05:03 +05:30
parent d71db351ca
commit e2f83f49da
No known key found for this signature in database
GPG key ID: 0E155DAD31671DA3
106 changed files with 417 additions and 418 deletions

View file

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

View file

@ -0,0 +1,205 @@
package org.fossify.messages.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 org.fossify.commons.extensions.showErrorToast
import org.fossify.commons.extensions.toast
import org.fossify.messages.R
import org.fossify.messages.extensions.getThreadId
import org.fossify.messages.extensions.isPlainTextMimeType
import org.fossify.messages.extensions.smsSender
import org.fossify.messages.messaging.SmsException.Companion.ERROR_PERSISTING_MESSAGE
import org.fossify.messages.models.Attachment
import org.fossify.messages.receivers.MmsSentReceiver
import org.fossify.messages.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, messageId: Long? = null
): 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 {
if (messageId != null) {
val selection = "${Sms._ID} = ?"
val selectionArgs = arrayOf(messageId.toString())
val count = context.contentResolver.update(Sms.CONTENT_URI, values, selection, selectionArgs)
if (count > 0) {
response = Uri.parse("${Sms.CONTENT_URI}/${messageId}")
} else {
response = null
}
} else {
response = context.contentResolver.insert(Sms.CONTENT_URI, values)
}
} catch (e: Exception) {
throw SmsException(ERROR_PERSISTING_MESSAGE, e)
}
return response ?: throw SmsException(ERROR_PERSISTING_MESSAGE)
}
/** Send an SMS message given [text] and [addresses]. A [SmsException] is thrown in case any errors occur. */
fun sendSmsMessage(
text: String, addresses: Set<String>, subId: Int, requireDeliveryReport: Boolean, messageId: Long? = null
) {
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,
messageId = messageId
)
}
for (address in addresses) {
val threadId = context.getThreadId(address)
val messageUri = insertSmsMessage(
subId = subId, dest = address, text = text,
timestamp = System.currentTimeMillis(), threadId = threadId,
messageId = messageId
)
try {
context.smsSender.sendMessage(
subId = subId, destination = address, body = text, serviceCenter = null,
requireDeliveryReport = requireDeliveryReport, messageUri = messageUri
)
} catch (e: Exception) {
updateSmsMessageSendingStatus(messageUri, Sms.Outbox.MESSAGE_TYPE_FAILED)
throw e // propagate error to caller
}
}
}
fun updateSmsMessageSendingStatus(messageUri: Uri?, type: Int) {
val resolver = context.contentResolver
val values = ContentValues().apply {
put(Sms.Outbox.TYPE, type)
}
try {
if (messageUri != null) {
resolver.update(messageUri, values, null, null)
} else {
// mark latest sms as sent, need to check if this is still necessary (or reliable)
// as this was taken from android-smsmms. The messageUri shouldn't be null anyway
val cursor = resolver.query(Sms.Outbox.CONTENT_URI, null, null, null, null)
cursor?.use {
if (cursor.moveToFirst()) {
@SuppressLint("Range")
val id = cursor.getString(cursor.getColumnIndex(Sms.Outbox._ID))
val selection = "${Sms._ID} = ?"
val selectionArgs = arrayOf(id.toString())
resolver.update(Sms.Outbox.CONTENT_URI, values, selection, selectionArgs)
}
}
}
} catch (e: Exception) {
context.showErrorToast(e)
}
}
fun getSmsMessageFromDeliveryReport(intent: Intent): SmsMessage? {
val pdu = intent.getByteArrayExtra("pdu")
val format = intent.getStringExtra("format")
return SmsMessage.createFromPdu(pdu, format)
}
@Deprecated("TODO: Move/rewrite MMS code into the app.")
fun sendMmsMessage(text: String, addresses: List<String>, attachment: Attachment?, settings: Settings, messageId: Long? = null) {
val transaction = Transaction(context, settings)
val message = Message(text, addresses.toTypedArray())
if (attachment != null) {
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(org.fossify.commons.R.string.unknown_error_occurred))
}
}
val mmsSentIntent = Intent(context, MmsSentReceiver::class.java)
mmsSentIntent.putExtra(MmsSentReceiver.EXTRA_ORIGINAL_RESENT_MESSAGE_ID, messageId)
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)
SmsManager.RESULT_NO_DEFAULT_SMS_APP -> context.getString(R.string.sim_card_not_available)
else -> context.getString(R.string.unknown_error_occurred_sending_message, resultCode)
}
}
context.toast(msg = msg, length = Toast.LENGTH_LONG)
} else {
// no-op
}
}
companion object {
const val ADDRESS_SEPARATOR = "|"
}
}

View file

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

View file

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

View file

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

View file

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