sms-translate/app/src/main/kotlin/com/simplemobiletools/smsmessenger/extensions/Context.kt

964 lines
34 KiB
Kotlin

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<Message> {
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<String, Boolean>()
val blockedNumbers = getBlockedNumbers()
var messages = ArrayList<Message>()
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<Message> { it.date }.thenBy { it.id })
.toMutableList() as ArrayList<Message>
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<Message> {
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<String>? = 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<Message>()
val contactsMap = HashMap<Int, SimpleContact>()
val threadParticipants = HashMap<Long, ArrayList<SimpleContact>>()
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<SimpleContact> = ArrayList()): ArrayList<Conversation> {
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<Conversation>()
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<Long> {
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<Long>()
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<String>? = 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<Int, SimpleContact>?): ArrayList<SimpleContact> {
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<SimpleContact>()
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<Int>): ArrayList<String> {
val numbers = ArrayList<String>()
recipientIds.forEach {
numbers.add(getPhoneNumberFromAddressId(it))
}
return numbers
}
fun Context.getThreadContactNames(phoneNumbers: List<String>, privateContacts: ArrayList<SimpleContact>): ArrayList<String> {
val names = ArrayList<String>()
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<SimpleContact>): ArrayList<SimpleContact> {
val contacts = ArrayList<SimpleContact>()
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<Conversation>) {
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<String>): 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<Long, String?> {
val drafts = HashMap<Long, String?>()
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<Message>, newThreadId: Long) {
val scheduledMessages = messages.map { it.copy(threadId = newThreadId) }.toTypedArray()
messagesDB.insertMessages(*scheduledMessages)
}
fun Context.clearExpiredScheduledMessages(threadId: Long, messagesToDelete: List<Message>? = 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)