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 import android.database.Cursor import android.graphics.Bitmap import android.graphics.BitmapFactory import android.net.Uri import android.os.Handler import android.os.Looper import android.provider.ContactsContract.PhoneLookup import android.provider.OpenableColumns import android.provider.Telephony.* import android.telephony.SubscriptionManager import android.text.TextUtils import com.bumptech.glide.Glide import com.bumptech.glide.load.engine.DiskCacheStrategy import com.bumptech.glide.request.RequestOptions 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.databases.MessagesDatabase import com.simplemobiletools.smsmessenger.helpers.* import com.simplemobiletools.smsmessenger.helpers.AttachmentUtils.parseAttachmentNames 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 val Context.config: Config get() = Config.newInstance(applicationContext) fun Context.getMessagesDB() = MessagesDatabase.getInstance(this) val Context.conversationsDB: ConversationsDao get() = getMessagesDB().ConversationsDao() val Context.attachmentsDB: AttachmentsDao get() = getMessagesDB().AttachmentsDao() val Context.messageAttachmentsDB: MessageAttachmentsDao get() = getMessagesDB().MessageAttachmentsDao() 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, dateFrom: Int = -1, includeScheduledMessages: Boolean = true, limit: Int = MESSAGES_LIMIT ): 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 rangeQuery = if (dateFrom == -1) "" else "AND ${Sms.DATE} < ${dateFrom.toLong() * 1000}" val selection = "${Sms.THREAD_ID} = ? $rangeQuery" val selectionArgs = arrayOf(threadId.toString()) val sortOrder = "${Sms.DATE} DESC LIMIT $limit" 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 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()) } val isMMS = false val message = Message(id, body, type, status, ArrayList(participants), date, read, thread, isMMS, null, senderName, photoUri, subscriptionId) messages.add(message) } messages.addAll(getMMS(threadId, getImageResolutions, sortOrder, dateFrom)) if (includeScheduledMessages) { try { val scheduledMessages = messagesDB.getScheduledThreadMessages(threadId) messages.addAll(scheduledMessages) } catch (e: Exception) { e.printStackTrace() } } messages = messages .filter { it.participants.isNotEmpty() } .filterNot { it.isScheduled && it.millis() < System.currentTimeMillis() } .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, dateFrom: Int = -1): 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 ) var selection: String? = null var selectionArgs: Array? = null if (threadId == null && dateFrom != -1) { selection = "${Sms.DATE} < ${dateFrom.toLong()}" //Should not multiply 1000 here, because date in mms's database is different from sms's. } else if (threadId != null && dateFrom == -1) { selection = "${Sms.THREAD_ID} = ?" selectionArgs = arrayOf(threadId.toString()) } else if (threadId != null) { selection = "${Sms.THREAD_ID} = ? AND ${Sms.DATE} < ${dateFrom.toLong()}" selectionArgs = 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.isEmpty() || 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 attachmentNames: List? = null var attachmentCount = 0 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 attachmentName = attachmentNames?.getOrNull(attachmentCount) ?: "" val attachment = Attachment(partId, id, Uri.withAppendedPath(uri, partId.toString()).toString(), mimetype, 0, 0, attachmentName) messageAttachment.attachments.add(attachment) attachmentCount++ } else { val text = cursor.getStringValue(Mms.Part.TEXT) attachmentNames = parseAttachmentNames(text) } } 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 sortOrder = "${Sms.DATE} DESC LIMIT 50" val blockedNumbers = getBlockedNumbers() queryCursor(uri, projection, null, null, 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 try { contentResolver.delete(uri, selection, selectionArgs) } catch (e: Exception) { e.printStackTrace() } 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.deleteScheduledMessage(messageId: Long) { try { messagesDB.delete(messageId) } 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.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 try { Threads.getOrCreateThreadId(this, address) } catch (e: Exception) { 0L } } @SuppressLint("NewApi") fun Context.getThreadId(addresses: Set): Long { return try { Threads.getOrCreateThreadId(this, addresses) } catch (e: Exception) { 0L } } fun Context.showReceivedMessageNotification(address: String, body: String, threadId: Long, bitmap: Bitmap?) { val privateCursor = getMyContactsCursor(favoritesOnly = false, withPhoneNumbersOnly = true) ensureBackgroundThread { val senderName = getNameFromAddress(address, privateCursor) Handler(Looper.getMainLooper()).post { notificationHelper.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) } } } fun Context.getNotificationBitmap(photoUri: String): Bitmap? { val size = resources.getDimension(R.dimen.notification_large_icon_size).toInt() if (photoUri.isEmpty()) { return null } val options = RequestOptions() .diskCacheStrategy(DiskCacheStrategy.RESOURCE) .centerCrop() return try { Glide.with(this) .asBitmap() .load(photoUri) .apply(options) .apply(RequestOptions.circleCropTransform()) .into(size, size) .get() } catch (e: Exception) { null } } 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 { val cursor = contentResolver.query(uri, projection, null, null, null) cursor?.use { while (it.moveToNext()) { val threadId = it.getLongValue(Sms.THREAD_ID) val draft = it.getStringValue(Sms.BODY) if (draft != null) { 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] insertOrUpdateConversation(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 } } // fix a glitch at enabling Release version minifying from 5.12.3 // reset messages in 5.14.3 again, as PhoneNumber is no longer minified fun Context.clearAllMessagesIfNeeded() { if (!config.wasDbCleared) { ensureBackgroundThread { messagesDB.deleteAll() } config.wasDbCleared = true } } fun Context.subscriptionManagerCompat(): SubscriptionManager { return getSystemService(SubscriptionManager::class.java) } fun Context.insertOrUpdateConversation( conversation: Conversation, cachedConv: Conversation? = conversationsDB.getConversationWithThreadId(conversation.threadId) ) { val updatedConv = if (cachedConv != null) { val usesCustomTitle = cachedConv.usesCustomTitle val title = if (usesCustomTitle) { cachedConv.title } else { conversation.title } conversation.copy(title = title, usesCustomTitle = usesCustomTitle) } else { conversation } conversationsDB.insertOrUpdate(updatedConv) } fun Context.renameConversation(conversation: Conversation, newTitle: String): Conversation { val updatedConv = conversation.copy(title = newTitle, usesCustomTitle = true) try { conversationsDB.insertOrUpdate(updatedConv) } catch (e: Exception) { e.printStackTrace() } return updatedConv } fun Context.createTemporaryThread(message: Message, threadId: Long = generateRandomId(), cachedConv: Conversation?) { val simpleContactHelper = SimpleContactsHelper(this) val addresses = message.participants.getAddresses() val photoUri = if (addresses.size == 1) simpleContactHelper.getPhotoUriFromPhoneNumber(addresses.first()) else "" val title = if (cachedConv != null && cachedConv.usesCustomTitle) { cachedConv.title } else { message.participants.getThreadTitle() } val conversation = Conversation( threadId = threadId, snippet = message.body, date = message.date, read = true, title = title, photoUri = photoUri, isGroupConversation = addresses.size > 1, phoneNumber = addresses.first(), isScheduled = true, usesCustomTitle = cachedConv?.usesCustomTitle == true ) try { conversationsDB.insertOrUpdate(conversation) } catch (e: Exception) { e.printStackTrace() } } fun Context.updateScheduledMessagesThreadId(messages: List, newThreadId: Long) { val scheduledMessages = messages.map { it.copy(threadId = newThreadId) }.toTypedArray() messagesDB.insertMessages(*scheduledMessages) } fun Context.clearExpiredScheduledMessages(threadId: Long, messagesToDelete: List? = null) { val messages = messagesToDelete ?: messagesDB.getScheduledThreadMessages(threadId) val now = System.currentTimeMillis() + 500L try { messages.filter { it.isScheduled && it.millis() < now }.forEach { msg -> messagesDB.delete(msg.id) } if (messages.filterNot { it.isScheduled && it.millis() < now }.isEmpty()) { // delete empty temporary thread val conversation = conversationsDB.getConversationWithThreadId(threadId) if (conversation != null && conversation.isScheduled) { conversationsDB.deleteThreadId(threadId) } } } catch (e: Exception) { e.printStackTrace() return } } fun Context.getDefaultKeyboardHeight() = resources.getDimensionPixelSize(R.dimen.default_keyboard_height)