package com.simplemobiletools.smsmessenger.extensions import android.annotation.SuppressLint import android.app.Notification import android.app.NotificationChannel import android.app.NotificationManager import android.app.PendingIntent import android.content.ContentResolver import android.content.ContentValues import android.content.Context import android.content.Intent import android.database.Cursor import android.graphics.Bitmap import android.graphics.BitmapFactory import android.media.AudioAttributes import android.media.AudioManager import android.media.RingtoneManager import android.net.Uri import android.os.Bundle import android.os.Handler import android.os.Looper import android.provider.ContactsContract.PhoneLookup import android.provider.OpenableColumns import android.provider.Telephony.* import android.text.TextUtils import androidx.core.app.NotificationCompat import androidx.core.app.RemoteInput import com.klinker.android.send_message.Settings import com.simplemobiletools.commons.extensions.* import com.simplemobiletools.commons.helpers.* import com.simplemobiletools.commons.models.PhoneNumber import com.simplemobiletools.commons.models.SimpleContact import com.simplemobiletools.smsmessenger.R import com.simplemobiletools.smsmessenger.activities.ThreadActivity import com.simplemobiletools.smsmessenger.databases.MessagesDatabase import com.simplemobiletools.smsmessenger.helpers.* 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.models.* import com.simplemobiletools.smsmessenger.receivers.DirectReplyReceiver import com.simplemobiletools.smsmessenger.receivers.MarkAsReadReceiver import me.leolin.shortcutbadger.ShortcutBadger import java.io.FileNotFoundException val Context.config: Config get() = Config.newInstance(applicationContext) fun Context.getMessagessDB() = MessagesDatabase.getInstance(this) val Context.conversationsDB: ConversationsDao get() = getMessagessDB().ConversationsDao() val Context.attachmentsDB: AttachmentsDao get() = getMessagessDB().AttachmentsDao() val Context.messageAttachmentsDB: MessageAttachmentsDao get() = getMessagessDB().MessageAttachmentsDao() val Context.messagesDB: MessagesDao get() = getMessagessDB().MessagesDao() fun Context.getMessages(threadId: Long, getImageResolutions: Boolean): ArrayList { val uri = Sms.CONTENT_URI val projection = arrayOf( Sms._ID, Sms.BODY, Sms.TYPE, Sms.ADDRESS, Sms.DATE, Sms.READ, Sms.THREAD_ID, Sms.SUBSCRIPTION_ID, Sms.STATUS ) val selection = "${Sms.THREAD_ID} = ?" val selectionArgs = arrayOf(threadId.toString()) val sortOrder = "${Sms._ID} DESC LIMIT 100" val blockStatus = HashMap() val blockedNumbers = getBlockedNumbers() var messages = ArrayList() queryCursor(uri, projection, selection, selectionArgs, sortOrder, showErrors = true) { cursor -> val senderNumber = cursor.getStringValue(Sms.ADDRESS) ?: return@queryCursor val isNumberBlocked = if (blockStatus.containsKey(senderNumber)) { blockStatus[senderNumber]!! } else { val isBlocked = isNumberBlocked(senderNumber, blockedNumbers) blockStatus[senderNumber] = isBlocked isBlocked } if (isNumberBlocked) { return@queryCursor } val id = cursor.getLongValue(Sms._ID) val body = cursor.getStringValue(Sms.BODY) val type = cursor.getIntValue(Sms.TYPE) val namePhoto = getNameAndPhotoFromPhoneNumber(senderNumber) val senderName = namePhoto.name val photoUri = namePhoto.photoUri ?: "" val date = (cursor.getLongValue(Sms.DATE) / 1000).toInt() val read = cursor.getIntValue(Sms.READ) == 1 val thread = cursor.getLongValue(Sms.THREAD_ID) val subscriptionId = cursor.getIntValue(Sms.SUBSCRIPTION_ID) val status = cursor.getIntValue(Sms.STATUS) val phoneNumber = PhoneNumber(senderNumber, 0, "", senderNumber) val participant = SimpleContact(0, 0, senderName, photoUri, arrayListOf(phoneNumber), ArrayList(), ArrayList()) val isMMS = false val message = Message(id, body, type, status, arrayListOf(participant), date, read, thread, isMMS, null, senderName, photoUri, subscriptionId) messages.add(message) } messages.addAll(getMMS(threadId, getImageResolutions, sortOrder)) messages = messages.filter { it.participants.isNotEmpty() } .sortedWith(compareBy { it.date }.thenBy { it.id }).toMutableList() as ArrayList return messages } // as soon as a message contains multiple recipients it counts as an MMS instead of SMS fun Context.getMMS(threadId: Long? = null, getImageResolutions: Boolean = false, sortOrder: String? = null): ArrayList { val uri = Mms.CONTENT_URI val projection = arrayOf( Mms._ID, Mms.DATE, Mms.READ, Mms.MESSAGE_BOX, Mms.THREAD_ID, Mms.SUBSCRIPTION_ID, Mms.STATUS ) val selection = if (threadId == null) { null } else { "${Mms.THREAD_ID} = ?" } val selectionArgs = if (threadId == null) { null } else { arrayOf(threadId.toString()) } val messages = ArrayList() val contactsMap = HashMap() val threadParticipants = HashMap>() queryCursor(uri, projection, selection, selectionArgs, sortOrder, showErrors = true) { cursor -> val mmsId = cursor.getLongValue(Mms._ID) val type = cursor.getIntValue(Mms.MESSAGE_BOX) val date = cursor.getLongValue(Mms.DATE).toInt() val read = cursor.getIntValue(Mms.READ) == 1 val threadId = cursor.getLongValue(Mms.THREAD_ID) val subscriptionId = cursor.getIntValue(Mms.SUBSCRIPTION_ID) val status = cursor.getIntValue(Mms.STATUS) val participants = if (threadParticipants.containsKey(threadId)) { threadParticipants[threadId]!! } else { val parts = getThreadParticipants(threadId, contactsMap) threadParticipants[threadId] = parts parts } val isMMS = true val attachment = getMmsAttachment(mmsId, getImageResolutions) val body = attachment.text var senderName = "" var senderPhotoUri = "" if (type != Mms.MESSAGE_BOX_SENT && type != Mms.MESSAGE_BOX_FAILED) { val number = getMMSSender(mmsId) val namePhoto = getNameAndPhotoFromPhoneNumber(number) senderName = namePhoto.name senderPhotoUri = namePhoto.photoUri ?: "" } val message = Message(mmsId, body, type, status, participants, date, read, threadId, isMMS, attachment, senderName, senderPhotoUri, subscriptionId) messages.add(message) participants.forEach { contactsMap[it.rawId] = it } } return messages } fun Context.getMMSSender(msgId: Long): String { val uri = Uri.parse("${Mms.CONTENT_URI}/$msgId/addr") val projection = arrayOf( Mms.Addr.ADDRESS ) try { val cursor = contentResolver.query(uri, projection, null, null, null) cursor?.use { if (cursor.moveToFirst()) { return cursor.getStringValue(Mms.Addr.ADDRESS) } } } catch (ignored: Exception) { } return "" } fun Context.getConversations(threadId: Long? = null, privateContacts: ArrayList = ArrayList()): ArrayList { val uri = Uri.parse("${Threads.CONTENT_URI}?simple=true") val projection = arrayOf( Threads._ID, Threads.SNIPPET, Threads.DATE, Threads.READ, Threads.RECIPIENT_IDS ) var selection = "${Threads.MESSAGE_COUNT} > ?" var selectionArgs = arrayOf("0") if (threadId != null) { selection += " AND ${Threads._ID} = ?" selectionArgs = arrayOf("0", threadId.toString()) } val sortOrder = "${Threads.DATE} DESC" val conversations = ArrayList() val simpleContactHelper = SimpleContactsHelper(this) val blockedNumbers = getBlockedNumbers() queryCursor(uri, projection, selection, selectionArgs, sortOrder, true) { cursor -> val id = cursor.getLongValue(Threads._ID) var snippet = cursor.getStringValue(Threads.SNIPPET) ?: "" if (snippet.isEmpty()) { snippet = getThreadSnippet(id) } var date = cursor.getLongValue(Threads.DATE) if (date.toString().length > 10) { date /= 1000 } val rawIds = cursor.getStringValue(Threads.RECIPIENT_IDS) val recipientIds = rawIds.split(" ").filter { it.areDigitsOnly() }.map { it.toInt() }.toMutableList() val phoneNumbers = getThreadPhoneNumbers(recipientIds) if (phoneNumbers.any { isNumberBlocked(it, blockedNumbers) }) { return@queryCursor } val names = getThreadContactNames(phoneNumbers, privateContacts) val title = TextUtils.join(", ", names.toTypedArray()) val photoUri = if (phoneNumbers.size == 1) simpleContactHelper.getPhotoUriFromPhoneNumber(phoneNumbers.first()) else "" val isGroupConversation = phoneNumbers.size > 1 val read = cursor.getIntValue(Threads.READ) == 1 val conversation = Conversation(id, snippet, date.toInt(), read, title, photoUri, isGroupConversation, phoneNumbers.first()) conversations.add(conversation) } conversations.sortByDescending { it.date } return conversations } fun Context.getConversationIds(): List { val uri = Uri.parse("${Threads.CONTENT_URI}?simple=true") val projection = arrayOf(Threads._ID) val selection = "${Threads.MESSAGE_COUNT} > ?" val selectionArgs = arrayOf("0") val sortOrder = "${Threads.DATE} ASC" val conversationIds = mutableListOf() queryCursor(uri, projection, selection, selectionArgs, sortOrder, true) { cursor -> val id = cursor.getLongValue(Threads._ID) conversationIds.add(id) } return conversationIds } // based on https://stackoverflow.com/a/6446831/1967672 @SuppressLint("NewApi") fun Context.getMmsAttachment(id: Long, getImageResolutions: Boolean): MessageAttachment { val uri = if (isQPlus()) { Mms.Part.CONTENT_URI } else { Uri.parse("content://mms/part") } val projection = arrayOf( Mms._ID, Mms.Part.CONTENT_TYPE, Mms.Part.TEXT ) val selection = "${Mms.Part.MSG_ID} = ?" val selectionArgs = arrayOf(id.toString()) val messageAttachment = MessageAttachment(id, "", arrayListOf()) var attachmentName = "" queryCursor(uri, projection, selection, selectionArgs, showErrors = true) { cursor -> val partId = cursor.getLongValue(Mms._ID) val mimetype = cursor.getStringValue(Mms.Part.CONTENT_TYPE) if (mimetype == "text/plain") { messageAttachment.text = cursor.getStringValue(Mms.Part.TEXT) ?: "" } else if (mimetype.startsWith("image/") || mimetype.startsWith("video/")) { val fileUri = Uri.withAppendedPath(uri, partId.toString()) var width = 0 var height = 0 if (getImageResolutions) { try { val options = BitmapFactory.Options() options.inJustDecodeBounds = true BitmapFactory.decodeStream(contentResolver.openInputStream(fileUri), null, options) width = options.outWidth height = options.outHeight } catch (e: Exception) { } } val attachment = Attachment(partId, id, fileUri.toString(), mimetype, width, height, "") messageAttachment.attachments.add(attachment) } else if (mimetype != "application/smil") { val attachment = Attachment(partId, id, Uri.withAppendedPath(uri, partId.toString()).toString(), mimetype, 0, 0, attachmentName) messageAttachment.attachments.add(attachment) } else { val text = cursor.getStringValue(Mms.Part.TEXT) val cutName = text.substringAfter("ref src=\"").substringBefore("\"") if (cutName.isNotEmpty()) { attachmentName = cutName } } } return messageAttachment } fun Context.getLatestMMS(): Message? { val sortOrder = "${Mms.DATE} DESC LIMIT 1" return getMMS(sortOrder = sortOrder).firstOrNull() } fun Context.getThreadSnippet(threadId: Long): String { val sortOrder = "${Mms.DATE} DESC LIMIT 1" val latestMms = getMMS(threadId, false, sortOrder).firstOrNull() var snippet = latestMms?.body ?: "" val uri = Sms.CONTENT_URI val projection = arrayOf( Sms.BODY ) val selection = "${Sms.THREAD_ID} = ? AND ${Sms.DATE} > ?" val selectionArgs = arrayOf( threadId.toString(), latestMms?.date?.toString() ?: "0" ) try { val cursor = contentResolver.query(uri, projection, selection, selectionArgs, sortOrder) cursor?.use { if (cursor.moveToFirst()) { snippet = cursor.getStringValue(Sms.BODY) } } } catch (ignored: Exception) { } return snippet } fun Context.getMessageRecipientAddress(messageId: Long): String { val uri = Sms.CONTENT_URI val projection = arrayOf( Sms.ADDRESS ) val selection = "${Sms._ID} = ?" val selectionArgs = arrayOf(messageId.toString()) try { val cursor = contentResolver.query(uri, projection, selection, selectionArgs, null) cursor?.use { if (cursor.moveToFirst()) { return cursor.getStringValue(Sms.ADDRESS) } } } catch (e: Exception) { } return "" } fun Context.getThreadParticipants(threadId: Long, contactsMap: HashMap?): ArrayList { val uri = Uri.parse("${MmsSms.CONTENT_CONVERSATIONS_URI}?simple=true") val projection = arrayOf( ThreadsColumns.RECIPIENT_IDS ) val selection = "${Mms._ID} = ?" val selectionArgs = arrayOf(threadId.toString()) val participants = ArrayList() try { val cursor = contentResolver.query(uri, projection, selection, selectionArgs, null) cursor?.use { if (cursor.moveToFirst()) { val address = cursor.getStringValue(ThreadsColumns.RECIPIENT_IDS) address.split(" ").filter { it.areDigitsOnly() }.forEach { val addressId = it.toInt() if (contactsMap?.containsKey(addressId) == true) { participants.add(contactsMap[addressId]!!) return@forEach } val number = getPhoneNumberFromAddressId(addressId) val namePhoto = getNameAndPhotoFromPhoneNumber(number) val name = namePhoto.name val photoUri = namePhoto.photoUri ?: "" val phoneNumber = PhoneNumber(number, 0, "", number) val contact = SimpleContact(addressId, addressId, name, photoUri, arrayListOf(phoneNumber), ArrayList(), ArrayList()) participants.add(contact) } } } } catch (e: Exception) { showErrorToast(e) } return participants } fun Context.getThreadPhoneNumbers(recipientIds: List): ArrayList { val numbers = ArrayList() recipientIds.forEach { numbers.add(getPhoneNumberFromAddressId(it)) } return numbers } fun Context.getThreadContactNames(phoneNumbers: List, privateContacts: ArrayList): ArrayList { val names = ArrayList() phoneNumbers.forEach { number -> val name = SimpleContactsHelper(this).getNameFromPhoneNumber(number) if (name != number) { names.add(name) } else { val privateContact = privateContacts.firstOrNull { it.doesHavePhoneNumber(number) } if (privateContact == null) { names.add(name) } else { names.add(privateContact.name) } } } return names } fun Context.getPhoneNumberFromAddressId(canonicalAddressId: Int): String { val uri = Uri.withAppendedPath(MmsSms.CONTENT_URI, "canonical-addresses") val projection = arrayOf( Mms.Addr.ADDRESS ) val selection = "${Mms._ID} = ?" val selectionArgs = arrayOf(canonicalAddressId.toString()) try { val cursor = contentResolver.query(uri, projection, selection, selectionArgs, null) cursor?.use { if (cursor.moveToFirst()) { return cursor.getStringValue(Mms.Addr.ADDRESS) } } } catch (e: Exception) { showErrorToast(e) } return "" } fun Context.getSuggestedContacts(privateContacts: ArrayList): ArrayList { val contacts = ArrayList() val uri = Sms.CONTENT_URI val projection = arrayOf( Sms.ADDRESS ) val selection = "1 == 1) GROUP BY (${Sms.ADDRESS}" val selectionArgs = null val sortOrder = "${Sms.DATE} DESC LIMIT 20" val blockedNumbers = getBlockedNumbers() queryCursor(uri, projection, selection, selectionArgs, sortOrder, showErrors = true) { cursor -> val senderNumber = cursor.getStringValue(Sms.ADDRESS) ?: return@queryCursor val namePhoto = getNameAndPhotoFromPhoneNumber(senderNumber) var senderName = namePhoto.name var photoUri = namePhoto.photoUri ?: "" if (isNumberBlocked(senderNumber, blockedNumbers)) { return@queryCursor } else if (namePhoto.name == senderNumber) { if (privateContacts.isNotEmpty()) { val privateContact = privateContacts.firstOrNull { it.phoneNumbers.first().normalizedNumber == senderNumber } if (privateContact != null) { senderName = privateContact.name photoUri = privateContact.photoUri } else { return@queryCursor } } else { return@queryCursor } } val phoneNumber = PhoneNumber(senderNumber, 0, "", senderNumber) val contact = SimpleContact(0, 0, senderName, photoUri, arrayListOf(phoneNumber), ArrayList(), ArrayList()) if (!contacts.map { it.phoneNumbers.first().normalizedNumber.trimToComparableNumber() }.contains(senderNumber.trimToComparableNumber())) { contacts.add(contact) } } return contacts } fun Context.getNameAndPhotoFromPhoneNumber(number: String): NamePhoto { if (!hasPermission(PERMISSION_READ_CONTACTS)) { return NamePhoto(number, null) } val uri = Uri.withAppendedPath(PhoneLookup.CONTENT_FILTER_URI, Uri.encode(number)) val projection = arrayOf( PhoneLookup.DISPLAY_NAME, PhoneLookup.PHOTO_URI ) try { val cursor = contentResolver.query(uri, projection, null, null, null) cursor.use { if (cursor?.moveToFirst() == true) { val name = cursor.getStringValue(PhoneLookup.DISPLAY_NAME) val photoUri = cursor.getStringValue(PhoneLookup.PHOTO_URI) return NamePhoto(name, photoUri) } } } catch (e: Exception) { } return NamePhoto(number, null) } fun Context.insertNewSMS(address: String, subject: String, body: String, date: Long, read: Int, threadId: Long, type: Int, subscriptionId: Int): Long { val uri = Sms.CONTENT_URI val contentValues = ContentValues().apply { put(Sms.ADDRESS, address) put(Sms.SUBJECT, subject) put(Sms.BODY, body) put(Sms.DATE, date) put(Sms.READ, read) put(Sms.THREAD_ID, threadId) put(Sms.TYPE, type) put(Sms.SUBSCRIPTION_ID, subscriptionId) } return try { val newUri = contentResolver.insert(uri, contentValues) newUri?.lastPathSegment?.toLong() ?: 0L } catch (e: Exception) { 0L } } fun Context.deleteConversation(threadId: Long) { var uri = Sms.CONTENT_URI val selection = "${Sms.THREAD_ID} = ?" val selectionArgs = arrayOf(threadId.toString()) try { contentResolver.delete(uri, selection, selectionArgs) } catch (e: Exception) { showErrorToast(e) } uri = Mms.CONTENT_URI contentResolver.delete(uri, selection, selectionArgs) conversationsDB.deleteThreadId(threadId) messagesDB.deleteThreadMessages(threadId) } fun Context.deleteMessage(id: Long, isMMS: Boolean) { val uri = if (isMMS) Mms.CONTENT_URI else Sms.CONTENT_URI val selection = "${Sms._ID} = ?" val selectionArgs = arrayOf(id.toString()) try { contentResolver.delete(uri, selection, selectionArgs) messagesDB.delete(id) } catch (e: Exception) { showErrorToast(e) } } fun Context.markMessageRead(id: Long, isMMS: Boolean) { val uri = if (isMMS) Mms.CONTENT_URI else Sms.CONTENT_URI val contentValues = ContentValues().apply { put(Sms.READ, 1) put(Sms.SEEN, 1) } val selection = "${Sms._ID} = ?" val selectionArgs = arrayOf(id.toString()) contentResolver.update(uri, contentValues, selection, selectionArgs) messagesDB.markRead(id) } fun Context.markThreadMessagesRead(threadId: Long) { arrayOf(Sms.CONTENT_URI, Mms.CONTENT_URI).forEach { uri -> val contentValues = ContentValues().apply { put(Sms.READ, 1) put(Sms.SEEN, 1) } val selection = "${Sms.THREAD_ID} = ?" val selectionArgs = arrayOf(threadId.toString()) contentResolver.update(uri, contentValues, selection, selectionArgs) } messagesDB.markThreadRead(threadId) } fun Context.markThreadMessagesUnread(threadId: Long) { arrayOf(Sms.CONTENT_URI, Mms.CONTENT_URI).forEach { uri -> val contentValues = ContentValues().apply { put(Sms.READ, 0) put(Sms.SEEN, 0) } val selection = "${Sms.THREAD_ID} = ?" val selectionArgs = arrayOf(threadId.toString()) contentResolver.update(uri, contentValues, selection, selectionArgs) } } 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.updateMessageSubscriptionId(messageId: Long, subscriptionId: Int) { val uri = Sms.CONTENT_URI val contentValues = ContentValues().apply { put(Sms.SUBSCRIPTION_ID, subscriptionId) } val selection = "${Sms._ID} = ?" val selectionArgs = arrayOf(messageId.toString()) contentResolver.update(uri, contentValues, selection, selectionArgs) } fun Context.updateUnreadCountBadge(conversations: List) { val unreadCount = conversations.count { !it.read } if (unreadCount == 0) { ShortcutBadger.removeCount(this) } else { ShortcutBadger.applyCount(this, unreadCount) } } @SuppressLint("NewApi") fun Context.getThreadId(address: String): Long { return if (isMarshmallowPlus()) { try { Threads.getOrCreateThreadId(this, address) } catch (e: Exception) { 0L } } else { 0L } } @SuppressLint("NewApi") fun Context.getThreadId(addresses: Set): Long { return if (isMarshmallowPlus()) { try { Threads.getOrCreateThreadId(this, addresses) } catch (e: Exception) { 0L } } else { 0L } } fun Context.showReceivedMessageNotification(address: String, body: String, threadId: Long, bitmap: Bitmap?) { val privateCursor = getMyContactsCursor(false, true) ensureBackgroundThread { val senderName = getNameFromAddress(address, privateCursor) Handler(Looper.getMainLooper()).post { showMessageNotification(address, body, threadId, bitmap, senderName) } } } fun Context.getNameFromAddress(address: String, privateCursor: Cursor?): String { var sender = getNameAndPhotoFromPhoneNumber(address).name if (address == sender) { val privateContacts = MyContactsContentProvider.getSimpleContacts(this, privateCursor) sender = privateContacts.firstOrNull { it.doesHavePhoneNumber(address) }?.name ?: address } return sender } fun Context.getContactFromAddress(address: String, callback: ((contact: SimpleContact?) -> Unit)) { val privateCursor = getMyContactsCursor(false, true) SimpleContactsHelper(this).getAvailableContacts(false) { val contact = it.firstOrNull { it.doesHavePhoneNumber(address) } if (contact == null) { val privateContacts = MyContactsContentProvider.getSimpleContacts(this, privateCursor) val privateContact = privateContacts.firstOrNull { it.doesHavePhoneNumber(address) } callback(privateContact) } else { callback(contact) } } } @SuppressLint("NewApi") fun Context.showMessageNotification(address: String, body: String, threadId: Long, bitmap: Bitmap?, sender: String) { val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager val soundUri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION) if (isOreoPlus()) { val audioAttributes = AudioAttributes.Builder() .setUsage(AudioAttributes.USAGE_NOTIFICATION) .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION) .setLegacyStreamType(AudioManager.STREAM_NOTIFICATION) .build() val name = getString(R.string.channel_received_sms) val importance = NotificationManager.IMPORTANCE_HIGH NotificationChannel(NOTIFICATION_CHANNEL, name, importance).apply { setBypassDnd(false) enableLights(true) setSound(soundUri, audioAttributes) enableVibration(true) notificationManager.createNotificationChannel(this) } } val intent = Intent(this, ThreadActivity::class.java).apply { putExtra(THREAD_ID, threadId) } val pendingIntent = PendingIntent.getActivity(this, threadId.hashCode(), intent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_MUTABLE) val summaryText = getString(R.string.new_message) val markAsReadIntent = Intent(this, MarkAsReadReceiver::class.java).apply { action = MARK_AS_READ putExtra(THREAD_ID, threadId) } val markAsReadPendingIntent = PendingIntent.getBroadcast(this, threadId.hashCode(), markAsReadIntent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_MUTABLE) var replyAction: NotificationCompat.Action? = null if (isNougatPlus()) { val replyLabel = getString(R.string.reply) val remoteInput = RemoteInput.Builder(REPLY) .setLabel(replyLabel) .build() val replyIntent = Intent(this, DirectReplyReceiver::class.java).apply { putExtra(THREAD_ID, threadId) putExtra(THREAD_NUMBER, address) } val replyPendingIntent = PendingIntent.getBroadcast(applicationContext, threadId.hashCode(), replyIntent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_MUTABLE) replyAction = NotificationCompat.Action.Builder(R.drawable.ic_send_vector, replyLabel, replyPendingIntent) .addRemoteInput(remoteInput) .build() } val largeIcon = bitmap ?: SimpleContactsHelper(this).getContactLetterIcon(sender) val builder = NotificationCompat.Builder(this, NOTIFICATION_CHANNEL).apply { when (config.lockScreenVisibilitySetting) { LOCK_SCREEN_SENDER_MESSAGE -> { setLargeIcon(largeIcon) setStyle(getMessagesStyle(notificationManager, threadId, sender, body)) } LOCK_SCREEN_SENDER -> { setContentTitle(sender) setLargeIcon(largeIcon) setStyle(NotificationCompat.BigTextStyle().setSummaryText(summaryText).bigText(body)) } } color = getProperPrimaryColor() setSmallIcon(R.drawable.ic_messenger) setContentIntent(pendingIntent) priority = NotificationCompat.PRIORITY_MAX setDefaults(Notification.DEFAULT_LIGHTS) setCategory(Notification.CATEGORY_MESSAGE) setAutoCancel(true) setSound(soundUri, AudioManager.STREAM_NOTIFICATION) } if (replyAction != null && config.lockScreenVisibilitySetting == LOCK_SCREEN_SENDER_MESSAGE) { builder.addAction(replyAction) } builder.addAction(R.drawable.ic_check_vector, getString(R.string.mark_as_read), markAsReadPendingIntent) .setChannelId(NOTIFICATION_CHANNEL) notificationManager.notify(threadId.hashCode(), builder.build()) } private fun getMessagesStyle(notificationManager: NotificationManager, threadId: Long, sender: String, body: String): NotificationCompat.MessagingStyle { val oldMessages = getOldMessages(notificationManager, threadId) val messages = NotificationCompat.MessagingStyle(sender) oldMessages.forEach { messages.addMessage(it) } val currentMessage = NotificationCompat.MessagingStyle.Message(body, System.currentTimeMillis(), sender) messages.addMessage(currentMessage) return messages } private fun getOldMessages(notificationManager: NotificationManager, threadId: Long): List { if (!isNougatPlus()) { return arrayListOf() } val currentNotification = notificationManager.activeNotifications.find { it.id == threadId.hashCode() } return if (currentNotification != null) { val messages = currentNotification.notification.extras.getParcelableArray(NotificationCompat.EXTRA_MESSAGES) val result = arrayListOf() messages?.forEach { val bundle = it as Bundle val sender = bundle.getCharSequence("sender") val text = bundle.getCharSequence("text") val time = bundle.getLong("time") val message = NotificationCompat.MessagingStyle.Message(text, time, sender) result.add(message) } return result } else { arrayListOf() } } fun Context.removeDiacriticsIfNeeded(text: String): String { return if (config.useSimpleCharacters) text.normalizeString() else text } fun Context.getSmsDraft(threadId: Long): String? { val uri = Sms.Draft.CONTENT_URI val projection = arrayOf(Sms.BODY) val selection = "${Sms.THREAD_ID} = ?" val selectionArgs = arrayOf(threadId.toString()) try { val cursor = contentResolver.query(uri, projection, selection, selectionArgs, null) cursor.use { if (cursor?.moveToFirst() == true) { return cursor.getString(0) } } } catch (e: Exception) { } return null } fun Context.getAllDrafts(): HashMap { val drafts = HashMap() val uri = Sms.Draft.CONTENT_URI val projection = arrayOf(Sms.BODY, Sms.THREAD_ID) try { queryCursor(uri, projection) { cursor -> cursor.use { val threadId = cursor.getLongValue(Sms.THREAD_ID) val draft = cursor.getStringValue(Sms.BODY) ?: return@queryCursor drafts[threadId] = draft } } } catch (e: Exception) { } return drafts } fun Context.saveSmsDraft(body: String, threadId: Long) { val uri = Sms.Draft.CONTENT_URI val contentValues = ContentValues().apply { put(Sms.BODY, body) put(Sms.DATE, System.currentTimeMillis().toString()) put(Sms.TYPE, Sms.MESSAGE_TYPE_DRAFT) put(Sms.THREAD_ID, threadId) } try { contentResolver.insert(uri, contentValues) } catch (e: Exception) { } } fun Context.deleteSmsDraft(threadId: Long) { val uri = Sms.Draft.CONTENT_URI val projection = arrayOf(Sms._ID) val selection = "${Sms.THREAD_ID} = ?" val selectionArgs = arrayOf(threadId.toString()) try { val cursor = contentResolver.query(uri, projection, selection, selectionArgs, null) cursor.use { if (cursor?.moveToFirst() == true) { val draftId = cursor.getLong(0) val draftUri = Uri.withAppendedPath(Sms.CONTENT_URI, "/${draftId}") contentResolver.delete(draftUri, null, null) } } } catch (e: Exception) { } } fun Context.updateLastConversationMessage(threadId: Long) { val uri = Threads.CONTENT_URI val selection = "${Threads._ID} = ?" val selectionArgs = arrayOf(threadId.toString()) try { contentResolver.delete(uri, selection, selectionArgs) val newConversation = getConversations(threadId)[0] conversationsDB.insertOrUpdate(newConversation) } catch (e: Exception) { } } fun Context.getFileSizeFromUri(uri: Uri): Long { val assetFileDescriptor = try { contentResolver.openAssetFileDescriptor(uri, "r") } catch (e: FileNotFoundException) { null } // uses ParcelFileDescriptor#getStatSize underneath if failed val length = assetFileDescriptor?.use { it.length } ?: FILE_SIZE_NONE if (length != -1L) { return length } // if "content://" uri scheme, try contentResolver table if (uri.scheme.equals(ContentResolver.SCHEME_CONTENT)) { return contentResolver.query(uri, arrayOf(OpenableColumns.SIZE), null, null, null)?.use { cursor -> // maybe shouldn't trust ContentResolver for size: // https://stackoverflow.com/questions/48302972/content-resolver-returns-wrong-size val sizeIndex = cursor.getColumnIndex(OpenableColumns.SIZE) if (sizeIndex == -1) { return@use FILE_SIZE_NONE } cursor.moveToFirst() return try { cursor.getLong(sizeIndex) } catch (_: Throwable) { FILE_SIZE_NONE } } ?: FILE_SIZE_NONE } else { return FILE_SIZE_NONE } } 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 }