Rename package to org.fossify.messages
This commit is contained in:
parent
d71db351ca
commit
e2f83f49da
106 changed files with 417 additions and 418 deletions
|
|
@ -0,0 +1,177 @@
|
|||
package org.fossify.messages.helpers
|
||||
|
||||
import android.app.Activity
|
||||
import android.net.Uri
|
||||
import org.fossify.commons.extensions.*
|
||||
import org.fossify.commons.helpers.SimpleContactsHelper
|
||||
import org.fossify.commons.helpers.ensureBackgroundThread
|
||||
import org.fossify.messages.R
|
||||
import org.fossify.messages.databinding.ItemAttachmentDocumentBinding
|
||||
import org.fossify.messages.databinding.ItemAttachmentDocumentPreviewBinding
|
||||
import org.fossify.messages.databinding.ItemAttachmentVcardBinding
|
||||
import org.fossify.messages.databinding.ItemAttachmentVcardPreviewBinding
|
||||
import org.fossify.messages.extensions.*
|
||||
|
||||
fun ItemAttachmentDocumentPreviewBinding.setupDocumentPreview(
|
||||
uri: Uri,
|
||||
title: String,
|
||||
mimeType: String,
|
||||
onClick: (() -> Unit)? = null,
|
||||
onLongClick: (() -> Unit)? = null,
|
||||
onRemoveButtonClicked: (() -> Unit)? = null
|
||||
) {
|
||||
documentAttachmentHolder.setupDocumentPreview(uri, title, mimeType, onClick, onLongClick)
|
||||
removeAttachmentButtonHolder.removeAttachmentButton.apply {
|
||||
beVisible()
|
||||
background.applyColorFilter(context.getProperPrimaryColor())
|
||||
if (onRemoveButtonClicked != null) {
|
||||
setOnClickListener {
|
||||
onRemoveButtonClicked.invoke()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun ItemAttachmentDocumentBinding.setupDocumentPreview(
|
||||
uri: Uri,
|
||||
title: String,
|
||||
mimeType: String,
|
||||
onClick: (() -> Unit)? = null,
|
||||
onLongClick: (() -> Unit)? = null
|
||||
) {
|
||||
val context = root.context
|
||||
if (title.isNotEmpty()) {
|
||||
filename.text = title
|
||||
}
|
||||
|
||||
ensureBackgroundThread {
|
||||
try {
|
||||
val size = context.getFileSizeFromUri(uri)
|
||||
root.post {
|
||||
fileSize.beVisible()
|
||||
fileSize.text = size.formatSize()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
root.post {
|
||||
fileSize.beGone()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val textColor = context.getProperTextColor()
|
||||
val primaryColor = context.getProperPrimaryColor()
|
||||
|
||||
filename.setTextColor(textColor)
|
||||
fileSize.setTextColor(textColor)
|
||||
|
||||
icon.setImageResource(getIconResourceForMimeType(mimeType))
|
||||
icon.background.setTint(primaryColor)
|
||||
root.background.applyColorFilter(primaryColor.darkenColor())
|
||||
|
||||
root.setOnClickListener {
|
||||
onClick?.invoke()
|
||||
}
|
||||
|
||||
root.setOnLongClickListener {
|
||||
onLongClick?.invoke()
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
fun ItemAttachmentVcardPreviewBinding.setupVCardPreview(
|
||||
activity: Activity,
|
||||
uri: Uri,
|
||||
onClick: (() -> Unit)? = null,
|
||||
onLongClick: (() -> Unit)? = null,
|
||||
onRemoveButtonClicked: (() -> Unit)? = null,
|
||||
) {
|
||||
vcardProgress.beVisible()
|
||||
vcardAttachmentHolder.setupVCardPreview(activity = activity, uri = uri, attachment = true, onClick = onClick, onLongClick = onLongClick) {
|
||||
vcardProgress.beGone()
|
||||
removeAttachmentButtonHolder.removeAttachmentButton.apply {
|
||||
beVisible()
|
||||
background.applyColorFilter(activity.getProperPrimaryColor())
|
||||
if (onRemoveButtonClicked != null) {
|
||||
setOnClickListener {
|
||||
onRemoveButtonClicked.invoke()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun ItemAttachmentVcardBinding.setupVCardPreview(
|
||||
activity: Activity,
|
||||
uri: Uri,
|
||||
attachment: Boolean = false,
|
||||
onClick: (() -> Unit)? = null,
|
||||
onLongClick: (() -> Unit)? = null,
|
||||
onVCardLoaded: (() -> Unit)? = null,
|
||||
) {
|
||||
val context = root.context
|
||||
val textColor = activity.getProperTextColor()
|
||||
val primaryColor = activity.getProperPrimaryColor()
|
||||
|
||||
root.background.applyColorFilter(primaryColor.darkenColor())
|
||||
vcardTitle.setTextColor(textColor)
|
||||
vcardSubtitle.setTextColor(textColor)
|
||||
|
||||
arrayOf(vcardPhoto, vcardTitle, vcardSubtitle, viewContactDetails).forEach {
|
||||
it.beGone()
|
||||
}
|
||||
|
||||
parseVCardFromUri(activity, uri) { vCards ->
|
||||
activity.runOnUiThread {
|
||||
if (vCards.isEmpty()) {
|
||||
vcardTitle.beVisible()
|
||||
vcardTitle.text = context.getString(org.fossify.commons.R.string.unknown_error_occurred)
|
||||
return@runOnUiThread
|
||||
}
|
||||
|
||||
val title = vCards.firstOrNull()?.parseNameFromVCard()
|
||||
val imageIcon = if (title != null) {
|
||||
SimpleContactsHelper(activity).getContactLetterIcon(title)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
arrayOf(vcardPhoto, vcardTitle).forEach {
|
||||
it.beVisible()
|
||||
}
|
||||
|
||||
vcardPhoto.setImageBitmap(imageIcon)
|
||||
vcardTitle.text = title
|
||||
|
||||
if (vCards.size > 1) {
|
||||
vcardSubtitle.beVisible()
|
||||
val quantity = vCards.size - 1
|
||||
vcardSubtitle.text = context.resources.getQuantityString(R.plurals.and_other_contacts, quantity, quantity)
|
||||
} else {
|
||||
vcardSubtitle.beGone()
|
||||
}
|
||||
|
||||
if (attachment) {
|
||||
onVCardLoaded?.invoke()
|
||||
} else {
|
||||
viewContactDetails.setTextColor(primaryColor)
|
||||
viewContactDetails.beVisible()
|
||||
}
|
||||
|
||||
vcardAttachmentHolder.setOnClickListener {
|
||||
onClick?.invoke()
|
||||
}
|
||||
vcardAttachmentHolder.setOnLongClickListener {
|
||||
onLongClick?.invoke()
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getIconResourceForMimeType(mimeType: String) = when {
|
||||
mimeType.isAudioMimeType() -> R.drawable.ic_vector_audio_file
|
||||
mimeType.isCalendarMimeType() -> R.drawable.ic_calendar_month_vector
|
||||
mimeType.isPdfMimeType() -> R.drawable.ic_vector_pdf
|
||||
mimeType.isZipMimeType() -> R.drawable.ic_vector_folder_zip
|
||||
else -> R.drawable.ic_document_vector
|
||||
}
|
||||
|
|
@ -0,0 +1,74 @@
|
|||
package org.fossify.messages.helpers
|
||||
|
||||
import android.util.Xml
|
||||
import org.xmlpull.v1.XmlPullParser
|
||||
|
||||
object AttachmentUtils {
|
||||
|
||||
fun parseAttachmentNames(text: String): List<String> {
|
||||
val parser = Xml.newPullParser()
|
||||
parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, false)
|
||||
parser.setInput(text.reader())
|
||||
parser.nextTag()
|
||||
return readSmil(parser)
|
||||
}
|
||||
|
||||
private fun readSmil(parser: XmlPullParser): List<String> {
|
||||
parser.require(XmlPullParser.START_TAG, null, "smil")
|
||||
while (parser.next() != XmlPullParser.END_TAG) {
|
||||
if (parser.eventType != XmlPullParser.START_TAG) {
|
||||
continue
|
||||
}
|
||||
|
||||
if (parser.name == "body") {
|
||||
return readBody(parser)
|
||||
} else {
|
||||
skip(parser)
|
||||
}
|
||||
}
|
||||
|
||||
return emptyList()
|
||||
}
|
||||
|
||||
private fun readBody(parser: XmlPullParser): List<String> {
|
||||
val names = mutableListOf<String>()
|
||||
parser.require(XmlPullParser.START_TAG, null, "body")
|
||||
while (parser.next() != XmlPullParser.END_TAG) {
|
||||
if (parser.eventType != XmlPullParser.START_TAG) {
|
||||
continue
|
||||
}
|
||||
|
||||
if (parser.name == "par") {
|
||||
parser.require(XmlPullParser.START_TAG, null, "par")
|
||||
while (parser.next() != XmlPullParser.END_TAG) {
|
||||
if (parser.eventType != XmlPullParser.START_TAG) {
|
||||
continue
|
||||
}
|
||||
|
||||
if (parser.name == "ref") {
|
||||
val value = parser.getAttributeValue(null, "src")
|
||||
names.add(value)
|
||||
parser.nextTag()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
skip(parser)
|
||||
}
|
||||
}
|
||||
return names
|
||||
}
|
||||
|
||||
private fun skip(parser: XmlPullParser) {
|
||||
if (parser.eventType != XmlPullParser.START_TAG) {
|
||||
throw IllegalStateException()
|
||||
}
|
||||
|
||||
var depth = 1
|
||||
while (depth != 0) {
|
||||
when (parser.next()) {
|
||||
XmlPullParser.END_TAG -> depth--
|
||||
XmlPullParser.START_TAG -> depth++
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
130
app/src/main/kotlin/org/fossify/messages/helpers/Config.kt
Normal file
130
app/src/main/kotlin/org/fossify/messages/helpers/Config.kt
Normal file
|
|
@ -0,0 +1,130 @@
|
|||
package org.fossify.messages.helpers
|
||||
|
||||
import android.content.Context
|
||||
import org.fossify.commons.helpers.BaseConfig
|
||||
import org.fossify.messages.extensions.getDefaultKeyboardHeight
|
||||
import org.fossify.messages.models.Conversation
|
||||
|
||||
class Config(context: Context) : BaseConfig(context) {
|
||||
companion object {
|
||||
fun newInstance(context: Context) = Config(context)
|
||||
}
|
||||
|
||||
fun saveUseSIMIdAtNumber(number: String, SIMId: Int) {
|
||||
prefs.edit().putInt(USE_SIM_ID_PREFIX + number, SIMId).apply()
|
||||
}
|
||||
|
||||
fun getUseSIMIdAtNumber(number: String) = prefs.getInt(USE_SIM_ID_PREFIX + number, 0)
|
||||
|
||||
var showCharacterCounter: Boolean
|
||||
get() = prefs.getBoolean(SHOW_CHARACTER_COUNTER, false)
|
||||
set(showCharacterCounter) = prefs.edit().putBoolean(SHOW_CHARACTER_COUNTER, showCharacterCounter).apply()
|
||||
|
||||
var useSimpleCharacters: Boolean
|
||||
get() = prefs.getBoolean(USE_SIMPLE_CHARACTERS, false)
|
||||
set(useSimpleCharacters) = prefs.edit().putBoolean(USE_SIMPLE_CHARACTERS, useSimpleCharacters).apply()
|
||||
|
||||
var sendOnEnter: Boolean
|
||||
get() = prefs.getBoolean(SEND_ON_ENTER, false)
|
||||
set(sendOnEnter) = prefs.edit().putBoolean(SEND_ON_ENTER, sendOnEnter).apply()
|
||||
|
||||
var enableDeliveryReports: Boolean
|
||||
get() = prefs.getBoolean(ENABLE_DELIVERY_REPORTS, false)
|
||||
set(enableDeliveryReports) = prefs.edit().putBoolean(ENABLE_DELIVERY_REPORTS, enableDeliveryReports).apply()
|
||||
|
||||
var sendLongMessageMMS: Boolean
|
||||
get() = prefs.getBoolean(SEND_LONG_MESSAGE_MMS, false)
|
||||
set(sendLongMessageMMS) = prefs.edit().putBoolean(SEND_LONG_MESSAGE_MMS, sendLongMessageMMS).apply()
|
||||
|
||||
var sendGroupMessageMMS: Boolean
|
||||
get() = prefs.getBoolean(SEND_GROUP_MESSAGE_MMS, false)
|
||||
set(sendGroupMessageMMS) = prefs.edit().putBoolean(SEND_GROUP_MESSAGE_MMS, sendGroupMessageMMS).apply()
|
||||
|
||||
var lockScreenVisibilitySetting: Int
|
||||
get() = prefs.getInt(LOCK_SCREEN_VISIBILITY, LOCK_SCREEN_SENDER_MESSAGE)
|
||||
set(lockScreenVisibilitySetting) = prefs.edit().putInt(LOCK_SCREEN_VISIBILITY, lockScreenVisibilitySetting).apply()
|
||||
|
||||
var mmsFileSizeLimit: Long
|
||||
get() = prefs.getLong(MMS_FILE_SIZE_LIMIT, FILE_SIZE_600_KB)
|
||||
set(mmsFileSizeLimit) = prefs.edit().putLong(MMS_FILE_SIZE_LIMIT, mmsFileSizeLimit).apply()
|
||||
|
||||
var pinnedConversations: Set<String>
|
||||
get() = prefs.getStringSet(PINNED_CONVERSATIONS, HashSet<String>())!!
|
||||
set(pinnedConversations) = prefs.edit().putStringSet(PINNED_CONVERSATIONS, pinnedConversations).apply()
|
||||
|
||||
fun addPinnedConversationByThreadId(threadId: Long) {
|
||||
pinnedConversations = pinnedConversations.plus(threadId.toString())
|
||||
}
|
||||
|
||||
fun addPinnedConversations(conversations: List<Conversation>) {
|
||||
pinnedConversations = pinnedConversations.plus(conversations.map { it.threadId.toString() })
|
||||
}
|
||||
|
||||
fun removePinnedConversationByThreadId(threadId: Long) {
|
||||
pinnedConversations = pinnedConversations.minus(threadId.toString())
|
||||
}
|
||||
|
||||
fun removePinnedConversations(conversations: List<Conversation>) {
|
||||
pinnedConversations = pinnedConversations.minus(conversations.map { it.threadId.toString() })
|
||||
}
|
||||
|
||||
var blockedKeywords: Set<String>
|
||||
get() = prefs.getStringSet(BLOCKED_KEYWORDS, HashSet<String>())!!
|
||||
set(blockedKeywords) = prefs.edit().putStringSet(BLOCKED_KEYWORDS, blockedKeywords).apply()
|
||||
|
||||
fun addBlockedKeyword(keyword: String) {
|
||||
blockedKeywords = blockedKeywords.plus(keyword)
|
||||
}
|
||||
|
||||
fun removeBlockedKeyword(keyword: String) {
|
||||
blockedKeywords = blockedKeywords.minus(keyword)
|
||||
}
|
||||
|
||||
var exportSms: Boolean
|
||||
get() = prefs.getBoolean(EXPORT_SMS, true)
|
||||
set(exportSms) = prefs.edit().putBoolean(EXPORT_SMS, exportSms).apply()
|
||||
|
||||
var exportMms: Boolean
|
||||
get() = prefs.getBoolean(EXPORT_MMS, true)
|
||||
set(exportMms) = prefs.edit().putBoolean(EXPORT_MMS, exportMms).apply()
|
||||
|
||||
var importSms: Boolean
|
||||
get() = prefs.getBoolean(IMPORT_SMS, true)
|
||||
set(importSms) = prefs.edit().putBoolean(IMPORT_SMS, importSms).apply()
|
||||
|
||||
var importMms: Boolean
|
||||
get() = prefs.getBoolean(IMPORT_MMS, true)
|
||||
set(importMms) = prefs.edit().putBoolean(IMPORT_MMS, importMms).apply()
|
||||
|
||||
var wasDbCleared: Boolean
|
||||
get() = prefs.getBoolean(WAS_DB_CLEARED, false)
|
||||
set(wasDbCleared) = prefs.edit().putBoolean(WAS_DB_CLEARED, wasDbCleared).apply()
|
||||
|
||||
var keyboardHeight: Int
|
||||
get() = prefs.getInt(SOFT_KEYBOARD_HEIGHT, context.getDefaultKeyboardHeight())
|
||||
set(keyboardHeight) = prefs.edit().putInt(SOFT_KEYBOARD_HEIGHT, keyboardHeight).apply()
|
||||
|
||||
var useRecycleBin: Boolean
|
||||
get() = prefs.getBoolean(USE_RECYCLE_BIN, false)
|
||||
set(useRecycleBin) = prefs.edit().putBoolean(USE_RECYCLE_BIN, useRecycleBin).apply()
|
||||
|
||||
var lastRecycleBinCheck: Long
|
||||
get() = prefs.getLong(LAST_RECYCLE_BIN_CHECK, 0L)
|
||||
set(lastRecycleBinCheck) = prefs.edit().putLong(LAST_RECYCLE_BIN_CHECK, lastRecycleBinCheck).apply()
|
||||
|
||||
var isArchiveAvailable: Boolean
|
||||
get() = prefs.getBoolean(IS_ARCHIVE_AVAILABLE, true)
|
||||
set(isArchiveAvailable) = prefs.edit().putBoolean(IS_ARCHIVE_AVAILABLE, isArchiveAvailable).apply()
|
||||
|
||||
var customNotifications: Set<String>
|
||||
get() = prefs.getStringSet(CUSTOM_NOTIFICATIONS, HashSet<String>())!!
|
||||
set(customNotifications) = prefs.edit().putStringSet(CUSTOM_NOTIFICATIONS, customNotifications).apply()
|
||||
|
||||
fun addCustomNotificationsByThreadId(threadId: Long) {
|
||||
customNotifications = customNotifications.plus(threadId.toString())
|
||||
}
|
||||
|
||||
fun removeCustomNotificationsByThreadId(threadId: Long) {
|
||||
customNotifications = customNotifications.minus(threadId.toString())
|
||||
}
|
||||
}
|
||||
101
app/src/main/kotlin/org/fossify/messages/helpers/Constants.kt
Normal file
101
app/src/main/kotlin/org/fossify/messages/helpers/Constants.kt
Normal file
|
|
@ -0,0 +1,101 @@
|
|||
package org.fossify.messages.helpers
|
||||
|
||||
import org.fossify.messages.models.Events
|
||||
import org.greenrobot.eventbus.EventBus
|
||||
import org.joda.time.DateTime
|
||||
import org.joda.time.DateTimeZone
|
||||
import kotlin.math.abs
|
||||
import kotlin.random.Random
|
||||
|
||||
const val THREAD_ID = "thread_id"
|
||||
const val THREAD_TITLE = "thread_title"
|
||||
const val THREAD_TEXT = "thread_text"
|
||||
const val THREAD_NUMBER = "thread_number"
|
||||
const val THREAD_ATTACHMENT_URI = "thread_attachment_uri"
|
||||
const val THREAD_ATTACHMENT_URIS = "thread_attachment_uris"
|
||||
const val SEARCHED_MESSAGE_ID = "searched_message_id"
|
||||
const val USE_SIM_ID_PREFIX = "use_sim_id_"
|
||||
const val NOTIFICATION_CHANNEL = "simple_sms_messenger"
|
||||
const val SHOW_CHARACTER_COUNTER = "show_character_counter"
|
||||
const val USE_SIMPLE_CHARACTERS = "use_simple_characters"
|
||||
const val SEND_ON_ENTER = "send_on_enter"
|
||||
const val LOCK_SCREEN_VISIBILITY = "lock_screen_visibility"
|
||||
const val ENABLE_DELIVERY_REPORTS = "enable_delivery_reports"
|
||||
const val SEND_LONG_MESSAGE_MMS = "send_long_message_mms"
|
||||
const val SEND_GROUP_MESSAGE_MMS = "send_group_message_mms"
|
||||
const val MMS_FILE_SIZE_LIMIT = "mms_file_size_limit"
|
||||
const val PINNED_CONVERSATIONS = "pinned_conversations"
|
||||
const val BLOCKED_KEYWORDS = "blocked_keywords"
|
||||
const val EXPORT_SMS = "export_sms"
|
||||
const val EXPORT_MMS = "export_mms"
|
||||
const val JSON_FILE_EXTENSION = ".json"
|
||||
const val JSON_MIME_TYPE = "application/json"
|
||||
const val XML_MIME_TYPE = "text/xml"
|
||||
const val TXT_MIME_TYPE = "text/plain"
|
||||
const val IMPORT_SMS = "import_sms"
|
||||
const val IMPORT_MMS = "import_mms"
|
||||
const val WAS_DB_CLEARED = "was_db_cleared_4"
|
||||
const val EXTRA_VCARD_URI = "vcard"
|
||||
const val SCHEDULED_MESSAGE_ID = "scheduled_message_id"
|
||||
const val SOFT_KEYBOARD_HEIGHT = "soft_keyboard_height"
|
||||
const val IS_MMS = "is_mms"
|
||||
const val MESSAGE_ID = "message_id"
|
||||
const val USE_RECYCLE_BIN = "use_recycle_bin"
|
||||
const val LAST_RECYCLE_BIN_CHECK = "last_recycle_bin_check"
|
||||
const val IS_RECYCLE_BIN = "is_recycle_bin"
|
||||
const val IS_ARCHIVE_AVAILABLE = "is_archive_available"
|
||||
const val CUSTOM_NOTIFICATIONS = "custom_notifications"
|
||||
|
||||
private const val PATH = "org.fossify.org.fossify.messages.action."
|
||||
const val MARK_AS_READ = PATH + "mark_as_read"
|
||||
const val REPLY = PATH + "reply"
|
||||
|
||||
// view types for the thread list view
|
||||
const val THREAD_DATE_TIME = 1
|
||||
const val THREAD_RECEIVED_MESSAGE = 2
|
||||
const val THREAD_SENT_MESSAGE = 3
|
||||
const val THREAD_SENT_MESSAGE_ERROR = 4
|
||||
const val THREAD_SENT_MESSAGE_SENT = 5
|
||||
const val THREAD_SENT_MESSAGE_SENDING = 6
|
||||
const val THREAD_LOADING = 7
|
||||
|
||||
// view types for attachment list
|
||||
const val ATTACHMENT_DOCUMENT = 7
|
||||
const val ATTACHMENT_MEDIA = 8
|
||||
const val ATTACHMENT_VCARD = 9
|
||||
|
||||
// lock screen visibility constants
|
||||
const val LOCK_SCREEN_SENDER_MESSAGE = 1
|
||||
const val LOCK_SCREEN_SENDER = 2
|
||||
const val LOCK_SCREEN_NOTHING = 3
|
||||
|
||||
const val FILE_SIZE_NONE = -1L
|
||||
const val FILE_SIZE_100_KB = 102_400L
|
||||
const val FILE_SIZE_200_KB = 204_800L
|
||||
const val FILE_SIZE_300_KB = 307_200L
|
||||
const val FILE_SIZE_600_KB = 614_400L
|
||||
const val FILE_SIZE_1_MB = 1_048_576L
|
||||
const val FILE_SIZE_2_MB = 2_097_152L
|
||||
|
||||
const val MESSAGES_LIMIT = 30
|
||||
|
||||
// intent launch request codes
|
||||
const val PICK_PHOTO_INTENT = 42
|
||||
const val PICK_VIDEO_INTENT = 49
|
||||
const val PICK_SAVE_FILE_INTENT = 43
|
||||
const val CAPTURE_PHOTO_INTENT = 44
|
||||
const val CAPTURE_VIDEO_INTENT = 45
|
||||
const val CAPTURE_AUDIO_INTENT = 46
|
||||
const val PICK_DOCUMENT_INTENT = 47
|
||||
const val PICK_CONTACT_INTENT = 48
|
||||
|
||||
fun refreshMessages() {
|
||||
EventBus.getDefault().post(Events.RefreshMessages())
|
||||
}
|
||||
|
||||
/** Not to be used with real messages persisted in the telephony db. This is for internal use only (e.g. scheduled messages, notification ids etc). */
|
||||
fun generateRandomId(length: Int = 9): Long {
|
||||
val millis = DateTime.now(DateTimeZone.UTC).millis
|
||||
val random = abs(Random(millis).nextLong())
|
||||
return random.toString().takeLast(length).toLong()
|
||||
}
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
package org.fossify.messages.helpers
|
||||
|
||||
import androidx.room.TypeConverter
|
||||
import com.google.gson.Gson
|
||||
import com.google.gson.reflect.TypeToken
|
||||
import org.fossify.commons.models.SimpleContact
|
||||
import org.fossify.messages.models.Attachment
|
||||
import org.fossify.messages.models.MessageAttachment
|
||||
|
||||
class Converters {
|
||||
private val gson = Gson()
|
||||
private val attachmentType = object : TypeToken<List<Attachment>>() {}.type
|
||||
private val simpleContactType = object : TypeToken<List<SimpleContact>>() {}.type
|
||||
private val messageAttachmentType = object : TypeToken<MessageAttachment?>() {}.type
|
||||
|
||||
@TypeConverter
|
||||
fun jsonToAttachmentList(value: String) = gson.fromJson<ArrayList<Attachment>>(value, attachmentType)
|
||||
|
||||
@TypeConverter
|
||||
fun attachmentListToJson(list: ArrayList<Attachment>) = gson.toJson(list)
|
||||
|
||||
@TypeConverter
|
||||
fun jsonToSimpleContactList(value: String) = gson.fromJson<ArrayList<SimpleContact>>(value, simpleContactType)
|
||||
|
||||
@TypeConverter
|
||||
fun simpleContactListToJson(list: ArrayList<SimpleContact>) = gson.toJson(list)
|
||||
|
||||
@TypeConverter
|
||||
fun jsonToMessageAttachment(value: String) = gson.fromJson<MessageAttachment>(value, messageAttachmentType)
|
||||
|
||||
@TypeConverter
|
||||
fun messageAttachmentToJson(messageAttachment: MessageAttachment?) = gson.toJson(messageAttachment)
|
||||
}
|
||||
|
|
@ -0,0 +1,168 @@
|
|||
package org.fossify.messages.helpers
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.BitmapFactory
|
||||
import android.graphics.Matrix
|
||||
import android.media.ExifInterface
|
||||
import android.net.Uri
|
||||
import org.fossify.commons.extensions.getCompressionFormat
|
||||
import org.fossify.commons.extensions.getMyFileUri
|
||||
import org.fossify.commons.helpers.ensureBackgroundThread
|
||||
import org.fossify.messages.extensions.extension
|
||||
import org.fossify.messages.extensions.getExtensionFromMimeType
|
||||
import org.fossify.messages.extensions.getFileSizeFromUri
|
||||
import org.fossify.messages.extensions.isImageMimeType
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
/**
|
||||
* Compress image to a given size based on
|
||||
* [Compressor](https://github.com/zetbaitsu/Compressor/)
|
||||
* */
|
||||
class ImageCompressor(private val context: Context) {
|
||||
private val contentResolver = context.contentResolver
|
||||
private val outputDirectory = File(context.cacheDir, "compressed").apply {
|
||||
if (!exists()) {
|
||||
mkdirs()
|
||||
}
|
||||
}
|
||||
|
||||
private val minQuality = 30
|
||||
private val minResolution = 56
|
||||
private val scaleStepFactor = 0.6f // increase for more accurate file size at the cost increased computation
|
||||
|
||||
fun compressImage(uri: Uri, compressSize: Long, lossy: Boolean = compressSize < FILE_SIZE_1_MB, callback: (compressedFileUri: Uri?) -> Unit) {
|
||||
ensureBackgroundThread {
|
||||
try {
|
||||
val fileSize = context.getFileSizeFromUri(uri)
|
||||
if (fileSize > compressSize) {
|
||||
val mimeType = contentResolver.getType(uri)!!
|
||||
if (mimeType.isImageMimeType()) {
|
||||
val byteArray = contentResolver.openInputStream(uri)?.readBytes()!!
|
||||
var imageFile = File(outputDirectory, System.currentTimeMillis().toString().plus(mimeType.getExtensionFromMimeType()))
|
||||
imageFile.writeBytes(byteArray)
|
||||
val bitmap = loadBitmap(imageFile)
|
||||
val format = if (lossy) {
|
||||
Bitmap.CompressFormat.JPEG
|
||||
} else {
|
||||
imageFile.path.getCompressionFormat()
|
||||
}
|
||||
|
||||
// This quality approximation mostly works for smaller images but will fail with larger images.
|
||||
val compressionRatio = compressSize / fileSize.toDouble()
|
||||
val quality = maxOf((compressionRatio * 100).roundToInt(), minQuality)
|
||||
imageFile = overWrite(imageFile, bitmap, format = format, quality = quality)
|
||||
|
||||
// Even the highest quality images start to look ugly if we use 10 as the minimum quality,
|
||||
// so we better save some image quality and change resolution instead. This is time consuming
|
||||
// and mostly needed for very large images. Since there's no reliable way to predict the
|
||||
// required resolution, we'll just iterate and find the best result.
|
||||
if (imageFile.length() > compressSize) {
|
||||
var scaledWidth = bitmap.width
|
||||
var scaledHeight = bitmap.height
|
||||
|
||||
while (imageFile.length() > compressSize) {
|
||||
scaledWidth = (scaledWidth * scaleStepFactor).roundToInt()
|
||||
scaledHeight = (scaledHeight * scaleStepFactor).roundToInt()
|
||||
if (scaledHeight < minResolution && scaledWidth < minResolution) {
|
||||
break
|
||||
}
|
||||
|
||||
imageFile = decodeSampledBitmapFromFile(imageFile, scaledWidth, scaledHeight).run {
|
||||
determineImageRotation(imageFile, bitmap = this).run {
|
||||
overWrite(imageFile, bitmap = this, format = format, quality = quality)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
callback.invoke(context.getMyFileUri(imageFile))
|
||||
} else {
|
||||
callback.invoke(null)
|
||||
}
|
||||
} else {
|
||||
// no need to compress since the file is less than the compress size
|
||||
callback.invoke(uri)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
callback.invoke(null)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun overWrite(imageFile: File, bitmap: Bitmap, format: Bitmap.CompressFormat = imageFile.path.getCompressionFormat(), quality: Int = 100): File {
|
||||
val result = if (format == imageFile.path.getCompressionFormat()) {
|
||||
imageFile
|
||||
} else {
|
||||
File("${imageFile.absolutePath.substringBeforeLast(".")}.${format.extension()}")
|
||||
}
|
||||
imageFile.delete()
|
||||
saveBitmap(bitmap, result, format, quality)
|
||||
return result
|
||||
}
|
||||
|
||||
private fun saveBitmap(bitmap: Bitmap, destination: File, format: Bitmap.CompressFormat = destination.path.getCompressionFormat(), quality: Int = 100) {
|
||||
destination.parentFile?.mkdirs()
|
||||
var fileOutputStream: FileOutputStream? = null
|
||||
try {
|
||||
fileOutputStream = FileOutputStream(destination.absolutePath)
|
||||
bitmap.compress(format, quality, fileOutputStream)
|
||||
} finally {
|
||||
fileOutputStream?.run {
|
||||
flush()
|
||||
close()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadBitmap(imageFile: File) = BitmapFactory.decodeFile(imageFile.absolutePath).run {
|
||||
determineImageRotation(imageFile, this)
|
||||
}
|
||||
|
||||
private fun determineImageRotation(imageFile: File, bitmap: Bitmap): Bitmap {
|
||||
val exif = ExifInterface(imageFile.absolutePath)
|
||||
val orientation = exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, 0)
|
||||
val matrix = Matrix()
|
||||
when (orientation) {
|
||||
6 -> matrix.postRotate(90f)
|
||||
3 -> matrix.postRotate(180f)
|
||||
8 -> matrix.postRotate(270f)
|
||||
}
|
||||
return Bitmap.createBitmap(bitmap, 0, 0, bitmap.width, bitmap.height, matrix, true)
|
||||
}
|
||||
|
||||
private fun decodeSampledBitmapFromFile(imageFile: File, reqWidth: Int, reqHeight: Int): Bitmap {
|
||||
return BitmapFactory.Options().run {
|
||||
inJustDecodeBounds = true
|
||||
BitmapFactory.decodeFile(imageFile.absolutePath, this)
|
||||
|
||||
inSampleSize = calculateInSampleSize(this, reqWidth, reqHeight)
|
||||
|
||||
inJustDecodeBounds = false
|
||||
BitmapFactory.decodeFile(imageFile.absolutePath, this)
|
||||
}
|
||||
}
|
||||
|
||||
private fun calculateInSampleSize(options: BitmapFactory.Options, reqWidth: Int, reqHeight: Int): Int {
|
||||
// Raw height and width of image
|
||||
val height = options.outHeight
|
||||
val width = options.outWidth
|
||||
var inSampleSize = 1
|
||||
|
||||
if (height > reqHeight || width > reqWidth) {
|
||||
|
||||
val halfHeight: Int = height / 2
|
||||
val halfWidth: Int = width / 2
|
||||
|
||||
// Calculate the largest inSampleSize value that is a power of 2 and keeps both
|
||||
// height and width larger than the requested height and width.
|
||||
while (halfHeight / inSampleSize >= reqHeight && halfWidth / inSampleSize >= reqWidth) {
|
||||
inSampleSize *= 2
|
||||
}
|
||||
}
|
||||
|
||||
return inSampleSize
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,197 @@
|
|||
package org.fossify.messages.helpers
|
||||
|
||||
import android.net.Uri
|
||||
import android.util.Xml
|
||||
import kotlinx.serialization.SerializationException
|
||||
import kotlinx.serialization.json.Json
|
||||
import org.fossify.commons.extensions.showErrorToast
|
||||
import org.fossify.commons.extensions.toast
|
||||
import org.fossify.commons.helpers.ensureBackgroundThread
|
||||
import org.fossify.messages.activities.SimpleActivity
|
||||
import org.fossify.messages.dialogs.ImportMessagesDialog
|
||||
import org.fossify.messages.extensions.config
|
||||
import org.fossify.messages.models.*
|
||||
import org.xmlpull.v1.XmlPullParser
|
||||
import java.io.InputStream
|
||||
|
||||
|
||||
class MessagesImporter(private val activity: SimpleActivity) {
|
||||
|
||||
private val messageWriter = MessagesWriter(activity)
|
||||
private val config = activity.config
|
||||
private var messagesImported = 0
|
||||
private var messagesFailed = 0
|
||||
|
||||
fun importMessages(uri: Uri) {
|
||||
try {
|
||||
val fileType = activity.contentResolver.getType(uri).orEmpty()
|
||||
val isXml = isXmlMimeType(fileType) || (uri.path?.endsWith("txt") == true && isFileXml(uri))
|
||||
if (isXml) {
|
||||
activity.toast(org.fossify.commons.R.string.importing)
|
||||
getInputStreamFromUri(uri)!!.importXml()
|
||||
} else {
|
||||
importJson(uri)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
activity.showErrorToast(e)
|
||||
}
|
||||
}
|
||||
|
||||
private fun importJson(uri: Uri) {
|
||||
try {
|
||||
val jsonString = activity.contentResolver.openInputStream(uri)!!.use { inputStream ->
|
||||
inputStream.bufferedReader().readText()
|
||||
}
|
||||
|
||||
val deserializedList = Json.decodeFromString<List<MessagesBackup>>(jsonString)
|
||||
if (deserializedList.isEmpty()) {
|
||||
activity.toast(org.fossify.commons.R.string.no_entries_for_importing)
|
||||
return
|
||||
}
|
||||
ImportMessagesDialog(activity, deserializedList)
|
||||
} catch (e: SerializationException) {
|
||||
activity.toast(org.fossify.commons.R.string.invalid_file_format)
|
||||
} catch (e: IllegalArgumentException) {
|
||||
activity.toast(org.fossify.commons.R.string.invalid_file_format)
|
||||
} catch (e: Exception) {
|
||||
activity.showErrorToast(e)
|
||||
}
|
||||
}
|
||||
|
||||
fun restoreMessages(messagesBackup: List<MessagesBackup>, callback: (ImportResult) -> Unit) {
|
||||
ensureBackgroundThread {
|
||||
try {
|
||||
messagesBackup.forEach { message ->
|
||||
try {
|
||||
if (message.backupType == BackupType.SMS && config.importSms) {
|
||||
messageWriter.writeSmsMessage(message as SmsBackup)
|
||||
messagesImported++
|
||||
} else if (message.backupType == BackupType.MMS && config.importMms) {
|
||||
messageWriter.writeMmsMessage(message as MmsBackup)
|
||||
messagesImported++
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
activity.showErrorToast(e)
|
||||
messagesFailed++
|
||||
}
|
||||
}
|
||||
refreshMessages()
|
||||
} catch (e: Exception) {
|
||||
activity.showErrorToast(e)
|
||||
}
|
||||
|
||||
callback.invoke(
|
||||
when {
|
||||
messagesImported == 0 && messagesFailed == 0 -> ImportResult.IMPORT_NOTHING_NEW
|
||||
messagesFailed > 0 && messagesImported > 0 -> ImportResult.IMPORT_PARTIAL
|
||||
messagesFailed > 0 -> ImportResult.IMPORT_FAIL
|
||||
else -> ImportResult.IMPORT_OK
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun InputStream.importXml() {
|
||||
try {
|
||||
bufferedReader().use { reader ->
|
||||
val xmlParser = Xml.newPullParser().apply {
|
||||
setInput(reader)
|
||||
}
|
||||
|
||||
xmlParser.nextTag()
|
||||
xmlParser.require(XmlPullParser.START_TAG, null, "smses")
|
||||
|
||||
var depth = 1
|
||||
while (depth != 0) {
|
||||
when (xmlParser.next()) {
|
||||
XmlPullParser.END_TAG -> depth--
|
||||
XmlPullParser.START_TAG -> depth++
|
||||
}
|
||||
|
||||
if (xmlParser.eventType != XmlPullParser.START_TAG) {
|
||||
continue
|
||||
}
|
||||
|
||||
try {
|
||||
if (xmlParser.name == "sms") {
|
||||
if (config.importSms) {
|
||||
val message = xmlParser.readSms()
|
||||
messageWriter.writeSmsMessage(message)
|
||||
messagesImported++
|
||||
} else {
|
||||
xmlParser.skip()
|
||||
}
|
||||
} else {
|
||||
xmlParser.skip()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
activity.showErrorToast(e)
|
||||
messagesFailed++
|
||||
}
|
||||
}
|
||||
refreshMessages()
|
||||
}
|
||||
when {
|
||||
messagesFailed > 0 && messagesImported > 0 -> activity.toast(org.fossify.commons.R.string.importing_some_entries_failed)
|
||||
messagesFailed > 0 -> activity.toast(org.fossify.commons.R.string.importing_failed)
|
||||
else -> activity.toast(org.fossify.commons.R.string.importing_successful)
|
||||
}
|
||||
} catch (_: Exception) {
|
||||
activity.toast(org.fossify.commons.R.string.invalid_file_format)
|
||||
}
|
||||
}
|
||||
|
||||
private fun XmlPullParser.readSms(): SmsBackup {
|
||||
require(XmlPullParser.START_TAG, null, "sms")
|
||||
|
||||
return SmsBackup(
|
||||
subscriptionId = 0,
|
||||
address = getAttributeValue(null, "address"),
|
||||
body = getAttributeValue(null, "body"),
|
||||
date = getAttributeValue(null, "date").toLong(),
|
||||
dateSent = getAttributeValue(null, "date").toLong(),
|
||||
locked = getAttributeValue(null, "locked").toInt(),
|
||||
protocol = getAttributeValue(null, "protocol"),
|
||||
read = getAttributeValue(null, "read").toInt(),
|
||||
status = getAttributeValue(null, "status").toInt(),
|
||||
type = getAttributeValue(null, "type").toInt(),
|
||||
serviceCenter = getAttributeValue(null, "service_center")
|
||||
)
|
||||
}
|
||||
|
||||
private fun XmlPullParser.skip() {
|
||||
if (eventType != XmlPullParser.START_TAG) {
|
||||
throw IllegalStateException()
|
||||
}
|
||||
var depth = 1
|
||||
while (depth != 0) {
|
||||
when (next()) {
|
||||
XmlPullParser.END_TAG -> depth--
|
||||
XmlPullParser.START_TAG -> depth++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getInputStreamFromUri(uri: Uri): InputStream? {
|
||||
return try {
|
||||
activity.contentResolver.openInputStream(uri)
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private fun isFileXml(uri: Uri): Boolean {
|
||||
val inputStream = getInputStreamFromUri(uri)
|
||||
return inputStream?.bufferedReader()?.use { reader ->
|
||||
reader.readLine()?.startsWith("<?xml") ?: false
|
||||
} ?: false
|
||||
}
|
||||
|
||||
private fun isXmlMimeType(mimeType: String): Boolean {
|
||||
return mimeType.equals("application/xml", ignoreCase = true) || mimeType.equals("text/xml", ignoreCase = true)
|
||||
}
|
||||
|
||||
private fun isJsonMimeType(mimeType: String): Boolean {
|
||||
return mimeType.equals("application/json", ignoreCase = true)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,256 @@
|
|||
package org.fossify.messages.helpers
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import android.provider.Telephony.Mms
|
||||
import android.provider.Telephony.Sms
|
||||
import android.util.Base64
|
||||
import org.fossify.commons.extensions.*
|
||||
import org.fossify.commons.helpers.isQPlus
|
||||
import org.fossify.commons.helpers.isRPlus
|
||||
import org.fossify.messages.extensions.getConversationIds
|
||||
import org.fossify.messages.models.*
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
|
||||
class MessagesReader(private val context: Context) {
|
||||
|
||||
fun getMessagesToExport(
|
||||
getSms: Boolean, getMms: Boolean, callback: (messages: List<MessagesBackup>) -> Unit
|
||||
) {
|
||||
val conversationIds = context.getConversationIds()
|
||||
var smsMessages = listOf<SmsBackup>()
|
||||
var mmsMessages = listOf<MmsBackup>()
|
||||
|
||||
if (getSms) {
|
||||
smsMessages = getSmsMessages(conversationIds)
|
||||
}
|
||||
if (getMms) {
|
||||
mmsMessages = getMmsMessages(conversationIds)
|
||||
}
|
||||
callback(smsMessages + mmsMessages)
|
||||
}
|
||||
|
||||
private fun getSmsMessages(threadIds: List<Long>): List<SmsBackup> {
|
||||
val projection = arrayOf(
|
||||
Sms.SUBSCRIPTION_ID,
|
||||
Sms.ADDRESS,
|
||||
Sms.BODY,
|
||||
Sms.DATE,
|
||||
Sms.DATE_SENT,
|
||||
Sms.LOCKED,
|
||||
Sms.PROTOCOL,
|
||||
Sms.READ,
|
||||
Sms.STATUS,
|
||||
Sms.TYPE,
|
||||
Sms.SERVICE_CENTER
|
||||
)
|
||||
|
||||
val selection = "${Sms.THREAD_ID} = ?"
|
||||
val smsList = mutableListOf<SmsBackup>()
|
||||
|
||||
threadIds.map { it.toString() }.forEach { threadId ->
|
||||
context.queryCursor(Sms.CONTENT_URI, projection, selection, arrayOf(threadId)) { cursor ->
|
||||
val subscriptionId = cursor.getLongValue(Sms.SUBSCRIPTION_ID)
|
||||
val address = cursor.getStringValue(Sms.ADDRESS)
|
||||
val body = cursor.getStringValueOrNull(Sms.BODY)
|
||||
val date = cursor.getLongValue(Sms.DATE)
|
||||
val dateSent = cursor.getLongValue(Sms.DATE_SENT)
|
||||
val locked = cursor.getIntValue(Sms.DATE_SENT)
|
||||
val protocol = cursor.getStringValueOrNull(Sms.PROTOCOL)
|
||||
val read = cursor.getIntValue(Sms.READ)
|
||||
val status = cursor.getIntValue(Sms.STATUS)
|
||||
val type = cursor.getIntValue(Sms.TYPE)
|
||||
val serviceCenter = cursor.getStringValueOrNull(Sms.SERVICE_CENTER)
|
||||
smsList.add(SmsBackup(subscriptionId, address, body, date, dateSent, locked, protocol, read, status, type, serviceCenter))
|
||||
}
|
||||
}
|
||||
return smsList
|
||||
}
|
||||
|
||||
private fun getMmsMessages(threadIds: List<Long>, includeTextOnlyAttachment: Boolean = false): List<MmsBackup> {
|
||||
val projection = arrayOf(
|
||||
Mms._ID,
|
||||
Mms.CREATOR,
|
||||
Mms.CONTENT_TYPE,
|
||||
Mms.DELIVERY_REPORT,
|
||||
Mms.DATE,
|
||||
Mms.DATE_SENT,
|
||||
Mms.LOCKED,
|
||||
Mms.MESSAGE_TYPE,
|
||||
Mms.MESSAGE_BOX,
|
||||
Mms.READ,
|
||||
Mms.READ_REPORT,
|
||||
Mms.SEEN,
|
||||
Mms.TEXT_ONLY,
|
||||
Mms.STATUS,
|
||||
Mms.SUBJECT_CHARSET,
|
||||
Mms.SUBSCRIPTION_ID,
|
||||
Mms.TRANSACTION_ID
|
||||
)
|
||||
val selection = if (includeTextOnlyAttachment) {
|
||||
"${Mms.THREAD_ID} = ? AND ${Mms.TEXT_ONLY} = ?"
|
||||
} else {
|
||||
"${Mms.THREAD_ID} = ?"
|
||||
}
|
||||
val mmsList = mutableListOf<MmsBackup>()
|
||||
|
||||
threadIds.map { it.toString() }.forEach { threadId ->
|
||||
val selectionArgs = if (includeTextOnlyAttachment) {
|
||||
arrayOf(threadId, "1")
|
||||
} else {
|
||||
arrayOf(threadId)
|
||||
}
|
||||
context.queryCursor(Mms.CONTENT_URI, projection, selection, selectionArgs) { cursor ->
|
||||
val mmsId = cursor.getLongValue(Mms._ID)
|
||||
val creator = cursor.getStringValueOrNull(Mms.CREATOR)
|
||||
val contentType = cursor.getStringValueOrNull(Mms.CONTENT_TYPE)
|
||||
val deliveryReport = cursor.getIntValue(Mms.DELIVERY_REPORT)
|
||||
val date = cursor.getLongValue(Mms.DATE)
|
||||
val dateSent = cursor.getLongValue(Mms.DATE_SENT)
|
||||
val locked = cursor.getIntValue(Mms.LOCKED)
|
||||
val messageType = cursor.getIntValue(Mms.MESSAGE_TYPE)
|
||||
val messageBox = cursor.getIntValue(Mms.MESSAGE_BOX)
|
||||
val read = cursor.getIntValue(Mms.READ)
|
||||
val readReport = cursor.getIntValue(Mms.READ_REPORT)
|
||||
val seen = cursor.getIntValue(Mms.SEEN)
|
||||
val textOnly = cursor.getIntValue(Mms.TEXT_ONLY)
|
||||
val status = cursor.getStringValueOrNull(Mms.STATUS)
|
||||
val subject = cursor.getStringValueOrNull(Mms.SUBJECT)
|
||||
val subjectCharSet = cursor.getStringValueOrNull(Mms.SUBJECT_CHARSET)
|
||||
val subscriptionId = cursor.getLongValue(Mms.SUBSCRIPTION_ID)
|
||||
val transactionId = cursor.getStringValueOrNull(Mms.TRANSACTION_ID)
|
||||
|
||||
val parts = getParts(mmsId)
|
||||
val addresses = getMmsAddresses(mmsId)
|
||||
mmsList.add(
|
||||
MmsBackup(
|
||||
creator,
|
||||
contentType,
|
||||
deliveryReport,
|
||||
date,
|
||||
dateSent,
|
||||
locked,
|
||||
messageType,
|
||||
messageBox,
|
||||
read,
|
||||
readReport,
|
||||
seen,
|
||||
textOnly,
|
||||
status,
|
||||
subject,
|
||||
subjectCharSet,
|
||||
subscriptionId,
|
||||
transactionId,
|
||||
addresses,
|
||||
parts
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
return mmsList
|
||||
}
|
||||
|
||||
@SuppressLint("NewApi")
|
||||
private fun getParts(mmsId: Long): List<MmsPart> {
|
||||
val parts = mutableListOf<MmsPart>()
|
||||
val uri = if (isQPlus()) Mms.Part.CONTENT_URI else Uri.parse("content://mms/part")
|
||||
val projection = arrayOf(
|
||||
Mms.Part._ID,
|
||||
Mms.Part.CONTENT_DISPOSITION,
|
||||
Mms.Part.CHARSET,
|
||||
Mms.Part.CONTENT_ID,
|
||||
Mms.Part.CONTENT_LOCATION,
|
||||
Mms.Part.CONTENT_TYPE,
|
||||
Mms.Part.CT_START,
|
||||
Mms.Part.CT_TYPE,
|
||||
Mms.Part.FILENAME,
|
||||
Mms.Part.NAME,
|
||||
Mms.Part.SEQ,
|
||||
Mms.Part.TEXT
|
||||
)
|
||||
|
||||
val selection = "${Mms.Part.MSG_ID} = ?"
|
||||
val selectionArgs = arrayOf(mmsId.toString())
|
||||
context.queryCursor(uri, projection, selection, selectionArgs) { cursor ->
|
||||
val partId = cursor.getLongValue(Mms.Part._ID)
|
||||
val contentDisposition = cursor.getStringValueOrNull(Mms.Part.CONTENT_DISPOSITION)
|
||||
val charset = cursor.getStringValueOrNull(Mms.Part.CHARSET)
|
||||
val contentId = cursor.getStringValueOrNull(Mms.Part.CONTENT_ID)
|
||||
val contentLocation = cursor.getStringValueOrNull(Mms.Part.CONTENT_LOCATION)
|
||||
val contentType = cursor.getStringValue(Mms.Part.CONTENT_TYPE)
|
||||
val ctStart = cursor.getStringValueOrNull(Mms.Part.CT_START)
|
||||
val ctType = cursor.getStringValueOrNull(Mms.Part.CT_TYPE)
|
||||
val filename = cursor.getStringValueOrNull(Mms.Part.FILENAME)
|
||||
val name = cursor.getStringValueOrNull(Mms.Part.NAME)
|
||||
val sequenceOrder = cursor.getIntValue(Mms.Part.SEQ)
|
||||
val text = cursor.getStringValueOrNull(Mms.Part.TEXT)
|
||||
val data = when {
|
||||
contentType.startsWith("text/") -> {
|
||||
usePart(partId) { stream ->
|
||||
stream.readBytes().toString(Charsets.UTF_8)
|
||||
}
|
||||
}
|
||||
|
||||
else -> {
|
||||
usePart(partId) { stream ->
|
||||
Base64.encodeToString(stream.readBytes(), Base64.DEFAULT)
|
||||
}
|
||||
}
|
||||
}
|
||||
parts.add(MmsPart(contentDisposition, charset, contentId, contentLocation, contentType, ctStart, ctType, filename, name, sequenceOrder, text, data))
|
||||
}
|
||||
return parts
|
||||
}
|
||||
|
||||
@SuppressLint("NewApi")
|
||||
private fun usePart(partId: Long, block: (InputStream) -> String): String {
|
||||
val partUri = if (isQPlus()) Mms.Part.CONTENT_URI.buildUpon().appendPath(partId.toString()).build() else Uri.parse("content://mms/part/$partId")
|
||||
try {
|
||||
val stream = context.contentResolver.openInputStream(partUri) ?: return ""
|
||||
stream.use {
|
||||
return block(stream)
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("NewApi")
|
||||
private fun getMmsAddresses(messageId: Long): List<MmsAddress> {
|
||||
val addresses = mutableListOf<MmsAddress>()
|
||||
val uri = if (isRPlus()) Mms.Addr.getAddrUriForMessage(messageId.toString()) else Uri.parse("content://mms/$messageId/addr")
|
||||
val projection = arrayOf(Mms.Addr.ADDRESS, Mms.Addr.TYPE, Mms.Addr.CHARSET)
|
||||
val selection = "${Mms.Addr.MSG_ID}= ?"
|
||||
val selectionArgs = arrayOf(messageId.toString())
|
||||
context.queryCursor(uri, projection, selection, selectionArgs) { cursor ->
|
||||
val address = cursor.getStringValue(Mms.Addr.ADDRESS)
|
||||
val type = cursor.getIntValue(Mms.Addr.TYPE)
|
||||
val charset = cursor.getIntValue(Mms.Addr.CHARSET)
|
||||
addresses.add(MmsAddress(address, type, charset))
|
||||
}
|
||||
return addresses
|
||||
}
|
||||
|
||||
fun getMessagesCount(): Int {
|
||||
return getSmsCount() + getMmsCount()
|
||||
}
|
||||
|
||||
fun getMmsCount(): Int {
|
||||
return countRows(Mms.CONTENT_URI)
|
||||
}
|
||||
|
||||
fun getSmsCount(): Int {
|
||||
return countRows(Sms.CONTENT_URI)
|
||||
}
|
||||
|
||||
private fun countRows(uri: Uri): Int {
|
||||
val cursor = context.contentResolver.query(
|
||||
uri, null, null, null, null
|
||||
) ?: return 0
|
||||
cursor.use {
|
||||
return cursor.count
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,155 @@
|
|||
package org.fossify.messages.helpers
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import android.provider.Telephony.Mms
|
||||
import android.provider.Telephony.Sms
|
||||
import android.util.Base64
|
||||
import com.google.android.mms.pdu_alt.PduHeaders
|
||||
import com.klinker.android.send_message.Utils
|
||||
import org.fossify.commons.extensions.getLongValue
|
||||
import org.fossify.commons.extensions.queryCursor
|
||||
import org.fossify.commons.helpers.isRPlus
|
||||
import org.fossify.messages.models.MmsAddress
|
||||
import org.fossify.messages.models.MmsBackup
|
||||
import org.fossify.messages.models.MmsPart
|
||||
import org.fossify.messages.models.SmsBackup
|
||||
|
||||
class MessagesWriter(private val context: Context) {
|
||||
private val INVALID_ID = -1L
|
||||
private val contentResolver = context.contentResolver
|
||||
|
||||
fun writeSmsMessage(smsBackup: SmsBackup) {
|
||||
val contentValues = smsBackup.toContentValues()
|
||||
val threadId = Utils.getOrCreateThreadId(context, smsBackup.address)
|
||||
contentValues.put(Sms.THREAD_ID, threadId)
|
||||
if (!smsExist(smsBackup)) {
|
||||
contentResolver.insert(Sms.CONTENT_URI, contentValues)
|
||||
}
|
||||
}
|
||||
|
||||
private fun smsExist(smsBackup: SmsBackup): Boolean {
|
||||
val uri = Sms.CONTENT_URI
|
||||
val projection = arrayOf(Sms._ID)
|
||||
val selection = "${Sms.DATE} = ? AND ${Sms.ADDRESS} = ? AND ${Sms.TYPE} = ?"
|
||||
val selectionArgs = arrayOf(smsBackup.date.toString(), smsBackup.address, smsBackup.type.toString())
|
||||
var exists = false
|
||||
context.queryCursor(uri, projection, selection, selectionArgs) {
|
||||
exists = it.count > 0
|
||||
}
|
||||
return exists
|
||||
}
|
||||
|
||||
fun writeMmsMessage(mmsBackup: MmsBackup) {
|
||||
// 1. write mms msg, get the msg_id, check if mms exists before writing
|
||||
// 2. write parts - parts depend on the msg id, check if part exist before writing, write data if it is a non-text part
|
||||
// 3. write the addresses, address depends on msg id too, check if address exist before writing
|
||||
val contentValues = mmsBackup.toContentValues()
|
||||
val threadId = getMmsThreadId(mmsBackup)
|
||||
if (threadId != INVALID_ID) {
|
||||
contentValues.put(Mms.THREAD_ID, threadId)
|
||||
if (!mmsExist(mmsBackup)) {
|
||||
contentResolver.insert(Mms.CONTENT_URI, contentValues)
|
||||
}
|
||||
val messageId = getMmsId(mmsBackup)
|
||||
if (messageId != INVALID_ID) {
|
||||
mmsBackup.parts.forEach { writeMmsPart(it, messageId) }
|
||||
mmsBackup.addresses.forEach { writeMmsAddress(it, messageId) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getMmsThreadId(mmsBackup: MmsBackup): Long {
|
||||
val address = when (mmsBackup.messageBox) {
|
||||
Mms.MESSAGE_BOX_INBOX -> mmsBackup.addresses.firstOrNull { it.type == PduHeaders.FROM }?.address
|
||||
else -> mmsBackup.addresses.firstOrNull { it.type == PduHeaders.TO }?.address
|
||||
}
|
||||
return if (!address.isNullOrEmpty()) {
|
||||
Utils.getOrCreateThreadId(context, address)
|
||||
} else {
|
||||
INVALID_ID
|
||||
}
|
||||
}
|
||||
|
||||
private fun getMmsId(mmsBackup: MmsBackup): Long {
|
||||
val threadId = getMmsThreadId(mmsBackup)
|
||||
val uri = Mms.CONTENT_URI
|
||||
val projection = arrayOf(Mms._ID)
|
||||
val selection = "${Mms.DATE} = ? AND ${Mms.DATE_SENT} = ? AND ${Mms.THREAD_ID} = ? AND ${Mms.MESSAGE_BOX} = ?"
|
||||
val selectionArgs = arrayOf(mmsBackup.date.toString(), mmsBackup.dateSent.toString(), threadId.toString(), mmsBackup.messageBox.toString())
|
||||
var id = INVALID_ID
|
||||
context.queryCursor(uri, projection, selection, selectionArgs) {
|
||||
id = it.getLongValue(Mms._ID)
|
||||
}
|
||||
|
||||
return id
|
||||
}
|
||||
|
||||
private fun mmsExist(mmsBackup: MmsBackup): Boolean {
|
||||
return getMmsId(mmsBackup) != INVALID_ID
|
||||
}
|
||||
|
||||
@SuppressLint("NewApi")
|
||||
private fun mmsAddressExist(mmsAddress: MmsAddress, messageId: Long): Boolean {
|
||||
val addressUri = if (isRPlus()) Mms.Addr.getAddrUriForMessage(messageId.toString()) else Uri.parse("content://mms/$messageId/addr")
|
||||
val projection = arrayOf(Mms.Addr._ID)
|
||||
val selection = "${Mms.Addr.TYPE} = ? AND ${Mms.Addr.ADDRESS} = ? AND ${Mms.Addr.MSG_ID} = ?"
|
||||
val selectionArgs = arrayOf(mmsAddress.type.toString(), mmsAddress.address.toString(), messageId.toString())
|
||||
var exists = false
|
||||
context.queryCursor(addressUri, projection, selection, selectionArgs) {
|
||||
exists = it.count > 0
|
||||
}
|
||||
return exists
|
||||
}
|
||||
|
||||
@SuppressLint("NewApi")
|
||||
private fun writeMmsAddress(mmsAddress: MmsAddress, messageId: Long) {
|
||||
if (!mmsAddressExist(mmsAddress, messageId)) {
|
||||
val addressUri = if (isRPlus()) {
|
||||
Mms.Addr.getAddrUriForMessage(messageId.toString())
|
||||
} else {
|
||||
Uri.parse("content://mms/$messageId/addr")
|
||||
}
|
||||
|
||||
val contentValues = mmsAddress.toContentValues()
|
||||
contentValues.put(Mms.Addr.MSG_ID, messageId)
|
||||
contentResolver.insert(addressUri, contentValues)
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("NewApi")
|
||||
private fun writeMmsPart(mmsPart: MmsPart, messageId: Long) {
|
||||
if (!mmsPartExist(mmsPart, messageId)) {
|
||||
val uri = Uri.parse("content://mms/${messageId}/part")
|
||||
val contentValues = mmsPart.toContentValues()
|
||||
contentValues.put(Mms.Part.MSG_ID, messageId)
|
||||
val partUri = contentResolver.insert(uri, contentValues)
|
||||
try {
|
||||
if (partUri != null) {
|
||||
if (mmsPart.isNonText()) {
|
||||
contentResolver.openOutputStream(partUri).use {
|
||||
val arr = Base64.decode(mmsPart.data, Base64.DEFAULT)
|
||||
it!!.write(arr)
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("NewApi")
|
||||
private fun mmsPartExist(mmsPart: MmsPart, messageId: Long): Boolean {
|
||||
val uri = Uri.parse("content://mms/${messageId}/part")
|
||||
val projection = arrayOf(Mms.Part._ID)
|
||||
val selection = "${Mms.Part.CONTENT_LOCATION} = ? AND ${Mms.Part.CONTENT_TYPE} = ? AND ${Mms.Part.MSG_ID} = ? AND ${Mms.Part.CONTENT_ID} = ?"
|
||||
val selectionArgs = arrayOf(mmsPart.contentLocation.toString(), mmsPart.contentType, messageId.toString(), mmsPart.contentId.toString())
|
||||
var exists = false
|
||||
context.queryCursor(uri, projection, selection, selectionArgs) {
|
||||
exists = it.count > 0
|
||||
}
|
||||
return exists
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,230 @@
|
|||
package org.fossify.messages.helpers
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Notification
|
||||
import android.app.NotificationChannel
|
||||
import android.app.NotificationManager.IMPORTANCE_HIGH
|
||||
import android.app.PendingIntent
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.graphics.Bitmap
|
||||
import android.media.AudioAttributes
|
||||
import android.media.AudioManager
|
||||
import android.media.RingtoneManager
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.app.Person
|
||||
import androidx.core.app.RemoteInput
|
||||
import org.fossify.commons.extensions.getProperPrimaryColor
|
||||
import org.fossify.commons.extensions.notificationManager
|
||||
import org.fossify.commons.helpers.SimpleContactsHelper
|
||||
import org.fossify.commons.helpers.isNougatPlus
|
||||
import org.fossify.commons.helpers.isOreoPlus
|
||||
import org.fossify.messages.R
|
||||
import org.fossify.messages.activities.ThreadActivity
|
||||
import org.fossify.messages.extensions.config
|
||||
import org.fossify.messages.messaging.isShortCodeWithLetters
|
||||
import org.fossify.messages.receivers.DeleteSmsReceiver
|
||||
import org.fossify.messages.receivers.DirectReplyReceiver
|
||||
import org.fossify.messages.receivers.MarkAsReadReceiver
|
||||
|
||||
class NotificationHelper(private val context: Context) {
|
||||
|
||||
private val notificationManager = context.notificationManager
|
||||
private val soundUri get() = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION)
|
||||
private val user = Person.Builder()
|
||||
.setName(context.getString(R.string.me))
|
||||
.build()
|
||||
|
||||
@SuppressLint("NewApi")
|
||||
fun showMessageNotification(
|
||||
messageId: Long,
|
||||
address: String,
|
||||
body: String,
|
||||
threadId: Long,
|
||||
bitmap: Bitmap?,
|
||||
sender: String?,
|
||||
alertOnlyOnce: Boolean = false
|
||||
) {
|
||||
val hasCustomNotifications = context.config.customNotifications.contains(threadId.toString())
|
||||
val notificationChannelId = if (hasCustomNotifications) threadId.toString() else NOTIFICATION_CHANNEL
|
||||
if (!hasCustomNotifications) {
|
||||
maybeCreateChannel(notificationChannelId, context.getString(R.string.channel_received_sms))
|
||||
}
|
||||
|
||||
val notificationId = threadId.hashCode()
|
||||
val contentIntent = Intent(context, ThreadActivity::class.java).apply {
|
||||
putExtra(THREAD_ID, threadId)
|
||||
}
|
||||
val contentPendingIntent =
|
||||
PendingIntent.getActivity(context, notificationId, contentIntent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_MUTABLE)
|
||||
|
||||
val markAsReadIntent = Intent(context, MarkAsReadReceiver::class.java).apply {
|
||||
action = MARK_AS_READ
|
||||
putExtra(THREAD_ID, threadId)
|
||||
}
|
||||
val markAsReadPendingIntent =
|
||||
PendingIntent.getBroadcast(context, notificationId, markAsReadIntent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_MUTABLE)
|
||||
|
||||
val deleteSmsIntent = Intent(context, DeleteSmsReceiver::class.java).apply {
|
||||
putExtra(THREAD_ID, threadId)
|
||||
putExtra(MESSAGE_ID, messageId)
|
||||
}
|
||||
val deleteSmsPendingIntent =
|
||||
PendingIntent.getBroadcast(context, notificationId, deleteSmsIntent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_MUTABLE)
|
||||
|
||||
var replyAction: NotificationCompat.Action? = null
|
||||
val isNoReplySms = isShortCodeWithLetters(address)
|
||||
if (isNougatPlus() && !isNoReplySms) {
|
||||
val replyLabel = context.getString(R.string.reply)
|
||||
val remoteInput = RemoteInput.Builder(REPLY)
|
||||
.setLabel(replyLabel)
|
||||
.build()
|
||||
|
||||
val replyIntent = Intent(context, DirectReplyReceiver::class.java).apply {
|
||||
putExtra(THREAD_ID, threadId)
|
||||
putExtra(THREAD_NUMBER, address)
|
||||
}
|
||||
|
||||
val replyPendingIntent =
|
||||
PendingIntent.getBroadcast(
|
||||
context.applicationContext,
|
||||
notificationId,
|
||||
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 ?: if (sender != null) {
|
||||
SimpleContactsHelper(context).getContactLetterIcon(sender)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
val builder = NotificationCompat.Builder(context, notificationChannelId).apply {
|
||||
when (context.config.lockScreenVisibilitySetting) {
|
||||
LOCK_SCREEN_SENDER_MESSAGE -> {
|
||||
setLargeIcon(largeIcon)
|
||||
setStyle(getMessagesStyle(address, body, notificationId, sender))
|
||||
}
|
||||
|
||||
LOCK_SCREEN_SENDER -> {
|
||||
setContentTitle(sender)
|
||||
setLargeIcon(largeIcon)
|
||||
val summaryText = context.getString(R.string.new_message)
|
||||
setStyle(NotificationCompat.BigTextStyle().setSummaryText(summaryText).bigText(body))
|
||||
}
|
||||
}
|
||||
|
||||
color = context.getProperPrimaryColor()
|
||||
setSmallIcon(R.drawable.ic_messenger)
|
||||
setContentIntent(contentPendingIntent)
|
||||
priority = NotificationCompat.PRIORITY_MAX
|
||||
setDefaults(Notification.DEFAULT_LIGHTS)
|
||||
setCategory(Notification.CATEGORY_MESSAGE)
|
||||
setAutoCancel(true)
|
||||
setOnlyAlertOnce(alertOnlyOnce)
|
||||
setSound(soundUri, AudioManager.STREAM_NOTIFICATION)
|
||||
}
|
||||
|
||||
if (replyAction != null && context.config.lockScreenVisibilitySetting == LOCK_SCREEN_SENDER_MESSAGE) {
|
||||
builder.addAction(replyAction)
|
||||
}
|
||||
|
||||
builder.addAction(org.fossify.commons.R.drawable.ic_check_vector, context.getString(R.string.mark_as_read), markAsReadPendingIntent)
|
||||
.setChannelId(notificationChannelId)
|
||||
if (isNoReplySms) {
|
||||
builder.addAction(
|
||||
org.fossify.commons.R.drawable.ic_delete_vector,
|
||||
context.getString(org.fossify.commons.R.string.delete),
|
||||
deleteSmsPendingIntent
|
||||
).setChannelId(notificationChannelId)
|
||||
}
|
||||
notificationManager.notify(notificationId, builder.build())
|
||||
}
|
||||
|
||||
@SuppressLint("NewApi")
|
||||
fun showSendingFailedNotification(recipientName: String, threadId: Long) {
|
||||
val hasCustomNotifications = context.config.customNotifications.contains(threadId.toString())
|
||||
val notificationChannelId = if (hasCustomNotifications) threadId.toString() else NOTIFICATION_CHANNEL
|
||||
if (!hasCustomNotifications) {
|
||||
maybeCreateChannel(notificationChannelId, context.getString(R.string.message_not_sent_short))
|
||||
}
|
||||
|
||||
val notificationId = generateRandomId().hashCode()
|
||||
val intent = Intent(context, ThreadActivity::class.java).apply {
|
||||
putExtra(THREAD_ID, threadId)
|
||||
}
|
||||
val contentPendingIntent = PendingIntent.getActivity(context, notificationId, intent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_MUTABLE)
|
||||
|
||||
val summaryText = String.format(context.getString(R.string.message_sending_error), recipientName)
|
||||
val largeIcon = SimpleContactsHelper(context).getContactLetterIcon(recipientName)
|
||||
val builder = NotificationCompat.Builder(context, notificationChannelId)
|
||||
.setContentTitle(context.getString(R.string.message_not_sent_short))
|
||||
.setContentText(summaryText)
|
||||
.setColor(context.getProperPrimaryColor())
|
||||
.setSmallIcon(R.drawable.ic_messenger)
|
||||
.setLargeIcon(largeIcon)
|
||||
.setStyle(NotificationCompat.BigTextStyle().bigText(summaryText))
|
||||
.setContentIntent(contentPendingIntent)
|
||||
.setPriority(NotificationCompat.PRIORITY_MAX)
|
||||
.setDefaults(Notification.DEFAULT_LIGHTS)
|
||||
.setCategory(Notification.CATEGORY_MESSAGE)
|
||||
.setAutoCancel(true)
|
||||
.setChannelId(notificationChannelId)
|
||||
|
||||
notificationManager.notify(notificationId, builder.build())
|
||||
}
|
||||
|
||||
private fun maybeCreateChannel(id: String, name: String) {
|
||||
if (isOreoPlus()) {
|
||||
val audioAttributes = AudioAttributes.Builder()
|
||||
.setUsage(AudioAttributes.USAGE_NOTIFICATION)
|
||||
.setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
|
||||
.setLegacyStreamType(AudioManager.STREAM_NOTIFICATION)
|
||||
.build()
|
||||
|
||||
val importance = IMPORTANCE_HIGH
|
||||
NotificationChannel(id, name, importance).apply {
|
||||
setBypassDnd(false)
|
||||
enableLights(true)
|
||||
setSound(soundUri, audioAttributes)
|
||||
enableVibration(true)
|
||||
notificationManager.createNotificationChannel(this)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getMessagesStyle(address: String, body: String, notificationId: Int, name: String?): NotificationCompat.MessagingStyle {
|
||||
val sender = if (name != null) {
|
||||
Person.Builder()
|
||||
.setName(name)
|
||||
.setKey(address)
|
||||
.build()
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
return NotificationCompat.MessagingStyle(user).also { style ->
|
||||
getOldMessages(notificationId).forEach {
|
||||
style.addMessage(it)
|
||||
}
|
||||
val newMessage = NotificationCompat.MessagingStyle.Message(body, System.currentTimeMillis(), sender)
|
||||
style.addMessage(newMessage)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getOldMessages(notificationId: Int): List<NotificationCompat.MessagingStyle.Message> {
|
||||
if (!isNougatPlus()) {
|
||||
return emptyList()
|
||||
}
|
||||
val currentNotification = notificationManager.activeNotifications.find { it.id == notificationId }
|
||||
return if (currentNotification != null) {
|
||||
val activeStyle = NotificationCompat.MessagingStyle.extractMessagingStyleFromNotification(currentNotification.notification)
|
||||
activeStyle?.messages.orEmpty()
|
||||
} else {
|
||||
emptyList()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
package org.fossify.messages.helpers
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import ezvcard.Ezvcard
|
||||
import ezvcard.VCard
|
||||
import org.fossify.commons.helpers.ensureBackgroundThread
|
||||
|
||||
fun parseVCardFromUri(context: Context, uri: Uri, callback: (vCards: List<VCard>) -> Unit) {
|
||||
ensureBackgroundThread {
|
||||
val inputStream = try {
|
||||
context.contentResolver.openInputStream(uri)
|
||||
} catch (e: Exception) {
|
||||
callback(emptyList())
|
||||
return@ensureBackgroundThread
|
||||
}
|
||||
val vCards = Ezvcard.parse(inputStream).all()
|
||||
callback(vCards)
|
||||
}
|
||||
}
|
||||
|
||||
fun VCard?.parseNameFromVCard(): String? {
|
||||
if (this == null) return null
|
||||
var fullName = formattedName?.value
|
||||
if (fullName.isNullOrEmpty()) {
|
||||
val structured = structuredName ?: return null
|
||||
val nameComponents = arrayListOf<String?>().apply {
|
||||
addAll(structured.prefixes)
|
||||
add(structured.given)
|
||||
addAll(structured.additionalNames)
|
||||
add(structured.family)
|
||||
addAll(structured.suffixes)
|
||||
}
|
||||
fullName = nameComponents.filter { !it.isNullOrEmpty() }.joinToString(separator = " ")
|
||||
}
|
||||
return fullName
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue