Merge branch 'master' into fix/wrong-sender-name

This commit is contained in:
Tibor Kaputa 2021-09-24 18:41:38 +02:00 committed by GitHub
commit 7fae2d8324
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
53 changed files with 1775 additions and 55 deletions

View file

@ -8,17 +8,23 @@ import android.content.pm.ShortcutInfo
import android.content.pm.ShortcutManager
import android.graphics.drawable.Icon
import android.graphics.drawable.LayerDrawable
import android.net.Uri
import android.os.Bundle
import android.provider.Telephony
import android.view.Menu
import android.view.MenuItem
import com.simplemobiletools.commons.dialogs.FilePickerDialog
import com.simplemobiletools.commons.extensions.*
import com.simplemobiletools.commons.helpers.*
import com.simplemobiletools.commons.models.FAQItem
import com.simplemobiletools.smsmessenger.BuildConfig
import com.simplemobiletools.smsmessenger.R
import com.simplemobiletools.smsmessenger.adapters.ConversationsAdapter
import com.simplemobiletools.smsmessenger.dialogs.ExportMessagesDialog
import com.simplemobiletools.smsmessenger.dialogs.ImportMessagesDialog
import com.simplemobiletools.smsmessenger.extensions.*
import com.simplemobiletools.smsmessenger.helpers.EXPORT_MIME_TYPE
import com.simplemobiletools.smsmessenger.helpers.MessagesExporter
import com.simplemobiletools.smsmessenger.helpers.THREAD_ID
import com.simplemobiletools.smsmessenger.helpers.THREAD_TITLE
import com.simplemobiletools.smsmessenger.models.Conversation
@ -27,14 +33,20 @@ import kotlinx.android.synthetic.main.activity_main.*
import org.greenrobot.eventbus.EventBus
import org.greenrobot.eventbus.Subscribe
import org.greenrobot.eventbus.ThreadMode
import java.io.FileOutputStream
import java.io.OutputStream
import java.util.*
import kotlin.collections.ArrayList
class MainActivity : SimpleActivity() {
private val MAKE_DEFAULT_APP_REQUEST = 1
private val PICK_IMPORT_SOURCE_INTENT = 11
private val PICK_EXPORT_FILE_INTENT = 21
private var storedTextColor = 0
private var storedFontSize = 0
private var bus: EventBus? = null
private val smsExporter by lazy { MessagesExporter(this) }
@SuppressLint("InlinedApi")
override fun onCreate(savedInstanceState: Bundle?) {
@ -108,6 +120,8 @@ class MainActivity : SimpleActivity() {
when (item.itemId) {
R.id.search -> launchSearch()
R.id.settings -> launchSettings()
R.id.export_messages -> tryToExportMessages()
R.id.import_messages -> tryImportMessages()
R.id.about -> launchAbout()
else -> return super.onOptionsItemSelected(item)
}
@ -122,6 +136,11 @@ class MainActivity : SimpleActivity() {
} else {
finish()
}
} else if (requestCode == PICK_IMPORT_SOURCE_INTENT && resultCode == Activity.RESULT_OK && resultData != null && resultData.data != null) {
tryImportMessagesFromFile(resultData.data!!)
} else if (requestCode == PICK_EXPORT_FILE_INTENT && resultCode == Activity.RESULT_OK && resultData != null && resultData.data != null) {
val outputStream = contentResolver.openOutputStream(resultData.data!!)
exportMessagesTo(outputStream)
}
}
@ -170,7 +189,7 @@ class MainActivity : SimpleActivity() {
private fun getCachedConversations() {
ensureBackgroundThread {
val conversations = try {
conversationsDB.getAll().sortedByDescending { it.date }.toMutableList() as ArrayList<Conversation>
conversationsDB.getAll().toMutableList() as ArrayList<Conversation>
} catch (e: Exception) {
ArrayList()
}
@ -226,6 +245,10 @@ class MainActivity : SimpleActivity() {
private fun setupConversations(conversations: ArrayList<Conversation>) {
val hasConversations = conversations.isNotEmpty()
val sortedConversations = conversations.sortedWith(
compareByDescending<Conversation> { config.pinnedConversations.contains(it.threadId.toString()) }
.thenByDescending { it.date }
).toMutableList() as ArrayList<Conversation>
conversations_list.beVisibleIf(hasConversations)
no_conversations_placeholder.beVisibleIf(!hasConversations)
no_conversations_placeholder_2.beVisibleIf(!hasConversations)
@ -237,7 +260,7 @@ class MainActivity : SimpleActivity() {
val currAdapter = conversations_list.adapter
if (currAdapter == null) {
ConversationsAdapter(this, conversations, conversations_list, conversations_fastscroller) {
ConversationsAdapter(this, sortedConversations, conversations_list, conversations_fastscroller) {
Intent(this, ThreadActivity::class.java).apply {
putExtra(THREAD_ID, (it as Conversation).threadId)
putExtra(THREAD_TITLE, it.title)
@ -254,7 +277,7 @@ class MainActivity : SimpleActivity() {
}
} else {
try {
(currAdapter as ConversationsAdapter).updateConversations(conversations)
(currAdapter as ConversationsAdapter).updateConversations(sortedConversations)
} catch (ignored: Exception) {
}
}
@ -318,6 +341,92 @@ class MainActivity : SimpleActivity() {
startAboutActivity(R.string.app_name, licenses, BuildConfig.VERSION_NAME, faqItems, true)
}
private fun tryToExportMessages() {
if (isQPlus()) {
ExportMessagesDialog(this, config.lastExportPath, true) { file ->
Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
type = EXPORT_MIME_TYPE
putExtra(Intent.EXTRA_TITLE, file.name)
addCategory(Intent.CATEGORY_OPENABLE)
startActivityForResult(this, PICK_EXPORT_FILE_INTENT)
}
}
} else {
handlePermission(PERMISSION_WRITE_STORAGE) {
if (it) {
ExportMessagesDialog(this, config.lastExportPath, false) { file ->
getFileOutputStream(file.toFileDirItem(this), true) { outStream ->
exportMessagesTo(outStream)
}
}
}
}
}
}
private fun exportMessagesTo(outputStream: OutputStream?) {
toast(R.string.exporting)
ensureBackgroundThread {
smsExporter.exportMessages(outputStream) {
val toastId = when (it) {
MessagesExporter.ExportResult.EXPORT_OK -> R.string.exporting_successful
else -> R.string.exporting_failed
}
toast(toastId)
}
}
}
private fun tryImportMessages() {
if (isQPlus()) {
Intent(Intent.ACTION_GET_CONTENT).apply {
addCategory(Intent.CATEGORY_OPENABLE)
type = EXPORT_MIME_TYPE
startActivityForResult(this, PICK_IMPORT_SOURCE_INTENT)
}
} else {
handlePermission(PERMISSION_READ_STORAGE) {
if (it) {
importEvents()
}
}
}
}
private fun importEvents() {
FilePickerDialog(this) {
showImportEventsDialog(it)
}
}
private fun showImportEventsDialog(path: String) {
ImportMessagesDialog(this, path)
}
private fun tryImportMessagesFromFile(uri: Uri) {
when (uri.scheme) {
"file" -> showImportEventsDialog(uri.path!!)
"content" -> {
val tempFile = getTempFile("messages", "backup.json")
if (tempFile == null) {
toast(R.string.unknown_error_occurred)
return
}
try {
val inputStream = contentResolver.openInputStream(uri)
val out = FileOutputStream(tempFile)
inputStream!!.copyTo(out)
showImportEventsDialog(tempFile.absolutePath)
} catch (e: Exception) {
showErrorToast(e)
}
}
else -> toast(R.string.invalid_file_format)
}
}
@Subscribe(threadMode = ThreadMode.MAIN)
fun refreshMessages(event: Events.RefreshMessages) {
initMessenger()

View file

@ -146,6 +146,7 @@ class ThreadActivity : SimpleActivity() {
}
updateMenuItemColors(menu)
checkPinBtnVisibility(menu)
return true
}
@ -159,6 +160,8 @@ class ThreadActivity : SimpleActivity() {
R.id.delete -> askConfirmDelete()
R.id.manage_people -> managePeople()
R.id.mark_as_unread -> markAsUnread()
R.id.pin_conversation -> pinConversation(true)
R.id.unpin_conversation -> pinConversation(false)
else -> return super.onOptionsItemSelected(item)
}
return true
@ -852,6 +855,24 @@ class ThreadActivity : SimpleActivity() {
return participants
}
private fun pinConversation(pin: Boolean) {
if (pin) {
config.addPinnedConversationByThreadId(threadId)
} else {
config.removePinnedConversationByThreadId(threadId)
}
runOnUiThread {
refreshMessages()
}
}
private fun checkPinBtnVisibility(menu: Menu) {
val pinnedConversations = config.pinnedConversations
menu.findItem(R.id.pin_conversation).isVisible = !pinnedConversations.contains(threadId.toString())
menu.findItem(R.id.unpin_conversation).isVisible = pinnedConversations.contains(threadId.toString())
}
@SuppressLint("MissingPermission")
@Subscribe(threadMode = ThreadMode.ASYNC)
fun refreshMessages(event: Events.RefreshMessages) {

View file

@ -22,10 +22,7 @@ import com.simplemobiletools.commons.views.FastScroller
import com.simplemobiletools.commons.views.MyRecyclerView
import com.simplemobiletools.smsmessenger.R
import com.simplemobiletools.smsmessenger.activities.SimpleActivity
import com.simplemobiletools.smsmessenger.extensions.deleteConversation
import com.simplemobiletools.smsmessenger.extensions.getSmsDraft
import com.simplemobiletools.smsmessenger.extensions.markThreadMessagesRead
import com.simplemobiletools.smsmessenger.extensions.markThreadMessagesUnread
import com.simplemobiletools.smsmessenger.extensions.*
import com.simplemobiletools.smsmessenger.helpers.refreshMessages
import com.simplemobiletools.smsmessenger.models.Conversation
import kotlinx.android.synthetic.main.item_conversation.view.*
@ -43,11 +40,16 @@ class ConversationsAdapter(
override fun getActionMenuId() = R.menu.cab_conversations
override fun prepareActionMode(menu: Menu) {
val selectedItems = getSelectedItems()
menu.apply {
findItem(R.id.cab_block_number).isVisible = isNougatPlus()
findItem(R.id.cab_add_number_to_contact).isVisible = isOneItemSelected() && getSelectedItems().firstOrNull()?.isGroupConversation == false
findItem(R.id.cab_dial_number).isVisible = isOneItemSelected() && getSelectedItems().firstOrNull()?.isGroupConversation == false
findItem(R.id.cab_copy_number).isVisible = isOneItemSelected() && getSelectedItems().firstOrNull()?.isGroupConversation == false
findItem(R.id.cab_add_number_to_contact).isVisible = isOneItemSelected() && selectedItems.firstOrNull()?.isGroupConversation == false
findItem(R.id.cab_dial_number).isVisible = isOneItemSelected() && selectedItems.firstOrNull()?.isGroupConversation == false
findItem(R.id.cab_copy_number).isVisible = isOneItemSelected() && selectedItems.firstOrNull()?.isGroupConversation == false
findItem(R.id.cab_mark_as_read).isVisible = selectedItems.any { !it.read }
findItem(R.id.cab_mark_as_unread).isVisible = selectedItems.any { it.read }
checkPinBtnVisibility(this)
}
}
@ -64,6 +66,8 @@ class ConversationsAdapter(
R.id.cab_delete -> askConfirmDelete()
R.id.cab_mark_as_read -> markAsRead()
R.id.cab_mark_as_unread -> markAsUnread()
R.id.cab_pin_conversation -> pinConversation(true)
R.id.cab_unpin_conversation -> pinConversation(false)
R.id.cab_select_all -> selectAll()
}
}
@ -237,6 +241,31 @@ class ConversationsAdapter(
private fun getSelectedItems() = conversations.filter { selectedKeys.contains(it.hashCode()) } as ArrayList<Conversation>
private fun pinConversation(pin: Boolean) {
val conversations = getSelectedItems()
if (conversations.isEmpty()) {
return
}
if (pin) {
activity.config.addPinnedConversations(conversations)
} else {
activity.config.removePinnedConversations(conversations)
}
activity.runOnUiThread {
refreshMessages()
finishActMode()
}
}
private fun checkPinBtnVisibility(menu: Menu) {
val pinnedConversations = activity.config.pinnedConversations
val selectedConversations = getSelectedItems()
menu.findItem(R.id.cab_pin_conversation).isVisible = selectedConversations.any { !pinnedConversations.contains(it.threadId.toString()) }
menu.findItem(R.id.cab_unpin_conversation).isVisible = selectedConversations.any { pinnedConversations.contains(it.threadId.toString()) }
}
override fun onViewRecycled(holder: ViewHolder) {
super.onViewRecycled(holder)
if (!activity.isDestroyed && !activity.isFinishing) {
@ -265,6 +294,8 @@ class ConversationsAdapter(
draft_indicator.beVisibleIf(smsDraft != null)
draft_indicator.setTextColor(adjustedPrimaryColor)
pin_indicator.beVisibleIf(activity.config.pinnedConversations.contains(conversation.threadId.toString()))
conversation_frame.isSelected = selectedKeys.contains(conversation.hashCode())
conversation_address.apply {

View file

@ -0,0 +1,77 @@
package com.simplemobiletools.smsmessenger.dialogs
import android.view.ViewGroup
import androidx.appcompat.app.AlertDialog
import com.simplemobiletools.commons.dialogs.FilePickerDialog
import com.simplemobiletools.commons.extensions.*
import com.simplemobiletools.smsmessenger.R
import com.simplemobiletools.smsmessenger.activities.SimpleActivity
import com.simplemobiletools.smsmessenger.extensions.config
import com.simplemobiletools.smsmessenger.helpers.EXPORT_FILE_EXT
import java.io.File
import kotlinx.android.synthetic.main.dialog_export_messages.view.*
class ExportMessagesDialog(
private val activity: SimpleActivity,
private val path: String,
private val hidePath: Boolean,
private val callback: (file: File) -> Unit,
) {
private var realPath = if (path.isEmpty()) activity.internalStoragePath else path
private val config = activity.config
init {
val view = (activity.layoutInflater.inflate(R.layout.dialog_export_messages, null) as ViewGroup).apply {
export_messages_folder.text = activity.humanizePath(realPath)
export_messages_filename.setText("${activity.getString(R.string.messages)}_${activity.getCurrentFormattedDateTime()}")
export_sms_checkbox.isChecked = config.exportSms
export_mms_checkbox.isChecked = config.exportMms
if (hidePath) {
export_messages_folder_label.beGone()
export_messages_folder.beGone()
} else {
export_messages_folder.setOnClickListener {
activity.hideKeyboard(export_messages_filename)
FilePickerDialog(activity, realPath, false, showFAB = true) {
export_messages_folder.text = activity.humanizePath(it)
realPath = it
}
}
}
}
AlertDialog.Builder(activity)
.setPositiveButton(R.string.ok, null)
.setNegativeButton(R.string.cancel, null)
.create().apply {
activity.setupDialogStuff(view, this, R.string.export_messages) {
getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener {
val filename = view.export_messages_filename.value
when {
filename.isEmpty() -> activity.toast(R.string.empty_name)
filename.isAValidFilename() -> {
val file = File(realPath, "$filename$EXPORT_FILE_EXT")
if (!hidePath && file.exists()) {
activity.toast(R.string.name_taken)
return@setOnClickListener
}
if(!view.export_sms_checkbox.isChecked && !view.export_mms_checkbox.isChecked){
activity.toast(R.string.export_unchecked_error_message)
return@setOnClickListener
}
config.exportSms = view.export_sms_checkbox.isChecked
config.exportMms = view.export_mms_checkbox.isChecked
config.lastExportPath = file.absolutePath.getParentPath()
callback(file)
dismiss()
}
else -> activity.toast(R.string.invalid_name)
}
}
}
}
}
}

View file

@ -0,0 +1,69 @@
package com.simplemobiletools.smsmessenger.dialogs
import android.view.ViewGroup
import androidx.appcompat.app.AlertDialog
import com.simplemobiletools.commons.extensions.setupDialogStuff
import com.simplemobiletools.commons.extensions.toast
import com.simplemobiletools.commons.helpers.ensureBackgroundThread
import com.simplemobiletools.smsmessenger.R
import com.simplemobiletools.smsmessenger.activities.SimpleActivity
import com.simplemobiletools.smsmessenger.extensions.config
import com.simplemobiletools.smsmessenger.helpers.MessagesImporter
import com.simplemobiletools.smsmessenger.helpers.MessagesImporter.ImportResult.IMPORT_OK
import com.simplemobiletools.smsmessenger.helpers.MessagesImporter.ImportResult.IMPORT_PARTIAL
import kotlinx.android.synthetic.main.dialog_import_messages.view.*
class ImportMessagesDialog(
private val activity: SimpleActivity,
private val path: String,
) {
private val config = activity.config
init {
var ignoreClicks = false
val view = (activity.layoutInflater.inflate(R.layout.dialog_import_messages, null) as ViewGroup).apply {
import_sms_checkbox.isChecked = config.importSms
import_mms_checkbox.isChecked = config.importMms
}
AlertDialog.Builder(activity)
.setPositiveButton(R.string.ok, null)
.setNegativeButton(R.string.cancel, null)
.create().apply {
activity.setupDialogStuff(view, this, R.string.import_messages) {
getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener {
if (ignoreClicks) {
return@setOnClickListener
}
if (!view.import_sms_checkbox.isChecked && !view.import_mms_checkbox.isChecked) {
activity.toast(R.string.import_unchecked_error_message)
return@setOnClickListener
}
ignoreClicks = true
activity.toast(R.string.importing)
config.importSms = view.import_sms_checkbox.isChecked
config.importMms = view.import_mms_checkbox.isChecked
ensureBackgroundThread {
MessagesImporter(activity).importMessages(path) {
handleParseResult(it)
dismiss()
}
}
}
}
}
}
private fun handleParseResult(result: MessagesImporter.ImportResult) {
activity.toast(
when (result) {
IMPORT_OK -> R.string.importing_successful
IMPORT_PARTIAL -> R.string.importing_some_entries_failed
else -> R.string.no_items_found
}
)
}
}

View file

@ -1,5 +1,7 @@
package com.simplemobiletools.smsmessenger.extensions
import android.content.ContentValues
inline fun <T> List<T>.indexOfFirstOrNull(predicate: (T) -> Boolean): Int? {
var index = 0
for (item in this) {
@ -9,3 +11,22 @@ inline fun <T> List<T>.indexOfFirstOrNull(predicate: (T) -> Boolean): Int? {
}
return null
}
fun Map<String, Any>.toContentValues(): ContentValues {
val contentValues = ContentValues()
for (item in entries) {
when (val value = item.value) {
is String -> contentValues.put(item.key, value)
is Byte -> contentValues.put(item.key, value)
is Short -> contentValues.put(item.key, value)
is Int -> contentValues.put(item.key, value)
is Long -> contentValues.put(item.key, value)
is Float -> contentValues.put(item.key, value)
is Double -> contentValues.put(item.key, value)
is Boolean -> contentValues.put(item.key, value)
is ByteArray -> contentValues.put(item.key, value)
}
}
return contentValues
}

View file

@ -255,6 +255,20 @@ fun Context.getConversations(threadId: Long? = null, privateContacts: ArrayList<
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): MessageAttachment {

View file

@ -0,0 +1,20 @@
package com.simplemobiletools.smsmessenger.extensions
import android.database.Cursor
import com.google.gson.JsonNull
import com.google.gson.JsonObject
fun Cursor.rowsToJson(): JsonObject {
val obj = JsonObject()
for (i in 0 until columnCount) {
val key = getColumnName(i)
when (getType(i)) {
Cursor.FIELD_TYPE_INTEGER -> obj.addProperty(key, getLong(i))
Cursor.FIELD_TYPE_FLOAT -> obj.addProperty(key, getFloat(i))
Cursor.FIELD_TYPE_STRING -> obj.addProperty(key, getString(i))
Cursor.FIELD_TYPE_NULL -> obj.add(key, JsonNull.INSTANCE)
}
}
return obj
}

View file

@ -0,0 +1,9 @@
package com.simplemobiletools.smsmessenger.extensions.gson
import com.google.gson.Gson
import com.google.gson.GsonBuilder
import com.google.gson.reflect.TypeToken
private val gsonBuilder = GsonBuilder().registerTypeAdapter(object: TypeToken<Map<String, Any>>(){}.type, MapDeserializerDoubleAsIntFix())
val gson : Gson = gsonBuilder.create()

View file

@ -0,0 +1,60 @@
package com.simplemobiletools.smsmessenger.extensions.gson
import com.google.gson.*
import java.math.BigDecimal
import java.math.BigInteger
val JsonElement.optString: String?
get() = safeConversion { asString }
val JsonElement.optLong: Long?
get() = safeConversion { asLong }
val JsonElement.optBoolean: Boolean?
get() = safeConversion { asBoolean }
val JsonElement.optFloat: Float?
get() = safeConversion { asFloat }
val JsonElement.optDouble: Double?
get() = safeConversion { asDouble }
val JsonElement.optJsonObject: JsonObject?
get() = safeConversion { asJsonObject }
val JsonElement.optJsonArray: JsonArray?
get() = safeConversion { asJsonArray }
val JsonElement.optJsonPrimitive: JsonPrimitive?
get() = safeConversion { asJsonPrimitive }
val JsonElement.optInt: Int?
get() = safeConversion { asInt }
val JsonElement.optBigDecimal: BigDecimal?
get() = safeConversion { asBigDecimal }
val JsonElement.optBigInteger: BigInteger?
get() = safeConversion { asBigInteger }
val JsonElement.optByte: Byte?
get() = safeConversion { asByte }
val JsonElement.optShort: Short?
get() = safeConversion { asShort }
val JsonElement.optJsonNull: JsonNull?
get() = safeConversion { asJsonNull }
val JsonElement.optCharacter: Char?
get() = safeConversion { asCharacter }
private fun <T> JsonElement.safeConversion(converter: () -> T?): T? {
return try {
converter()
} catch (e: Exception) {
null
}
}

View file

@ -0,0 +1,45 @@
package com.simplemobiletools.smsmessenger.extensions.gson
import com.google.gson.JsonArray
import com.google.gson.JsonElement
import com.google.gson.JsonObject
import com.google.gson.JsonPrimitive
fun JsonObject.optGet(key: String): JsonElement? = get(key)
fun JsonObject.optGetJsonArray(key: String): JsonArray? = getAsJsonArray(key)
fun JsonObject.optGetJsonObject(key: String): JsonObject? = getAsJsonObject(key)
fun JsonObject.optGetJsonPrimitive(key: String): JsonPrimitive? = getAsJsonPrimitive(key)
fun JsonObject.optString(key: String) = optGet(key)?.asString
fun JsonObject.optLong(key: String) = optGet(key)?.asLong
fun JsonObject.optBoolean(key: String) = optGet(key)?.asBoolean
fun JsonObject.optFloat(key: String) = optGet(key)?.asFloat
fun JsonObject.optDouble(key: String) = optGet(key)?.asDouble
fun JsonObject.optJsonObject(key: String) = optGet(key)?.asJsonObject
fun JsonObject.optJsonArray(key: String) = optGet(key)?.asJsonArray
fun JsonObject.optJsonPrimitive(key: String) = optGet(key)?.asJsonPrimitive
fun JsonObject.optInt(key: String) = optGet(key)?.asInt
fun JsonObject.optBigDecimal(key: String) = optGet(key)?.asBigDecimal
fun JsonObject.optBigInteger(key: String) = optGet(key)?.asBigInteger
fun JsonObject.optByte(key: String) = optGet(key)?.asByte
fun JsonObject.optShort(key: String) = optGet(key)?.asShort
fun JsonObject.optJsonNull(key: String) = optGet(key)?.asJsonNull
fun JsonObject.optCharacter(key: String) = optGet(key)?.asCharacter

View file

@ -0,0 +1,58 @@
package com.simplemobiletools.smsmessenger.extensions.gson
import com.google.gson.JsonDeserializationContext
import com.google.gson.JsonDeserializer
import com.google.gson.JsonElement
import com.google.gson.JsonParseException
import com.google.gson.internal.LinkedTreeMap
import java.lang.reflect.Type
import kotlin.math.ceil
// https://stackoverflow.com/a/36529534/10552591
class MapDeserializerDoubleAsIntFix : JsonDeserializer<Map<String, Any>?> {
@Throws(JsonParseException::class)
override fun deserialize(json: JsonElement, typeOfT: Type, context: JsonDeserializationContext): Map<String, Any>? {
return read(json) as Map<String, Any>?
}
fun read(element: JsonElement): Any? {
when {
element.isJsonArray -> {
val list: MutableList<Any?> = ArrayList()
val arr = element.asJsonArray
for (anArr in arr) {
list.add(read(anArr))
}
return list
}
element.isJsonObject -> {
val map: MutableMap<String, Any?> = LinkedTreeMap()
val obj = element.asJsonObject
val entitySet = obj.entrySet()
for ((key, value) in entitySet) {
map[key] = read(value)
}
return map
}
element.isJsonPrimitive -> {
val prim = element.asJsonPrimitive
when {
prim.isBoolean -> {
return prim.asBoolean
}
prim.isString -> {
return prim.asString
}
prim.isNumber -> {
val num = prim.asNumber
// here you can handle double int/long values
// and return any type you want
// this solution will transform 3.0 float to long values
return if (ceil(num.toDouble()) == num.toLong().toDouble()) num.toLong() else num.toDouble()
}
}
}
}
return null
}
}

View file

@ -2,6 +2,8 @@ package com.simplemobiletools.smsmessenger.helpers
import android.content.Context
import com.simplemobiletools.commons.helpers.BaseConfig
import com.simplemobiletools.smsmessenger.models.Conversation
import java.util.HashSet
class Config(context: Context) : BaseConfig(context) {
companion object {
@ -33,4 +35,44 @@ class Config(context: Context) : BaseConfig(context) {
var mmsFileSizeLimit: Long
get() = prefs.getLong(MMS_FILE_SIZE_LIMIT, FILE_SIZE_1_MB)
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 lastExportPath: String
get() = prefs.getString(LAST_EXPORT_PATH, "")!!
set(lastExportPath) = prefs.edit().putString(LAST_EXPORT_PATH, lastExportPath).apply()
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()
}

View file

@ -17,6 +17,14 @@ const val USE_SIMPLE_CHARACTERS = "use_simple_characters"
const val LOCK_SCREEN_VISIBILITY = "lock_screen_visibility"
const val ENABLE_DELIVERY_REPORTS = "enable_delivery_reports"
const val MMS_FILE_SIZE_LIMIT = "mms_file_size_limit"
const val PINNED_CONVERSATIONS = "pinned_conversations"
const val LAST_EXPORT_PATH = "last_export_path"
const val EXPORT_SMS = "export_sms"
const val EXPORT_MMS = "export_mms"
const val EXPORT_MIME_TYPE = "application/json"
const val EXPORT_FILE_EXT = ".json"
const val IMPORT_SMS = "import_sms"
const val IMPORT_MMS = "import_mms"
private const val PATH = "com.simplemobiletools.smsmessenger.action."
const val MARK_AS_READ = PATH + "mark_as_read"

View file

@ -0,0 +1,68 @@
package com.simplemobiletools.smsmessenger.helpers
import android.content.Context
import com.google.gson.Gson
import com.google.gson.stream.JsonWriter
import com.simplemobiletools.commons.helpers.ensureBackgroundThread
import com.simplemobiletools.smsmessenger.extensions.config
import com.simplemobiletools.smsmessenger.extensions.getConversationIds
import java.io.OutputStream
class MessagesExporter(private val context: Context) {
enum class ExportResult {
EXPORT_FAIL, EXPORT_OK
}
private val config = context.config
private val messageReader = MessagesReader(context)
private val gson = Gson()
fun exportMessages(outputStream: OutputStream?, onProgress: (total: Int, current: Int) -> Unit = { _, _ -> }, callback: (result: ExportResult) -> Unit) {
ensureBackgroundThread {
if (outputStream == null) {
callback.invoke(ExportResult.EXPORT_FAIL)
return@ensureBackgroundThread
}
val writer = JsonWriter(outputStream.bufferedWriter())
writer.use {
try {
var written = 0
writer.beginArray()
val conversationIds = context.getConversationIds()
val totalMessages = messageReader.getMessagesCount()
for (threadId in conversationIds) {
writer.beginObject()
if (config.exportSms && messageReader.getSmsCount() > 0) {
writer.name("sms")
writer.beginArray()
messageReader.forEachSms(threadId) {
writer.jsonValue(gson.toJson(it))
written++
onProgress.invoke(totalMessages, written)
}
writer.endArray()
}
if (config.exportMms && messageReader.getMmsCount() > 0) {
writer.name("mms")
writer.beginArray()
messageReader.forEachMms(threadId) {
writer.jsonValue(gson.toJson(it))
written++
onProgress.invoke(totalMessages, written)
}
writer.endArray()
}
writer.endObject()
}
writer.endArray()
callback.invoke(ExportResult.EXPORT_OK)
} catch (e: Exception) {
callback.invoke(ExportResult.EXPORT_FAIL)
}
}
}
}
}

View file

@ -0,0 +1,78 @@
package com.simplemobiletools.smsmessenger.helpers
import android.content.Context
import android.provider.Telephony.*
import com.google.gson.Gson
import com.google.gson.reflect.TypeToken
import com.simplemobiletools.commons.extensions.showErrorToast
import com.simplemobiletools.commons.helpers.ensureBackgroundThread
import com.simplemobiletools.smsmessenger.extensions.*
import com.simplemobiletools.smsmessenger.helpers.MessagesImporter.ImportResult.*
import com.simplemobiletools.smsmessenger.models.ExportedMessage
import java.io.File
class MessagesImporter(private val context: Context) {
enum class ImportResult {
IMPORT_FAIL, IMPORT_OK, IMPORT_PARTIAL, IMPORT_NOTHING_NEW
}
private val gson = Gson()
private val messageWriter = MessagesWriter(context)
private val config = context.config
private var messagesImported = 0
private var messagesFailed = 0
fun importMessages(path: String, onProgress: (total: Int, current: Int) -> Unit = { _, _ -> }, callback: (result: ImportResult) -> Unit) {
ensureBackgroundThread {
try {
val inputStream = if (path.contains("/")) {
File(path).inputStream()
} else {
context.assets.open(path)
}
inputStream.bufferedReader().use { reader ->
val json = reader.readText()
val type = object : TypeToken<List<ExportedMessage>>() {}.type
val messages = gson.fromJson<List<ExportedMessage>>(json, type)
val totalMessages = messages.flatMap { it.sms ?: emptyList() }.size + messages.flatMap { it.mms ?: emptyList() }.size
if (totalMessages <= 0) {
callback.invoke(IMPORT_NOTHING_NEW)
return@ensureBackgroundThread
}
onProgress.invoke(totalMessages, messagesImported)
for (message in messages) {
if (config.importSms) {
message.sms?.forEach { backup ->
messageWriter.writeSmsMessage(backup)
messagesImported++
onProgress.invoke(totalMessages, messagesImported)
}
}
if (config.importMms) {
message.mms?.forEach { backup ->
messageWriter.writeMmsMessage(backup)
messagesImported++
onProgress.invoke(totalMessages, messagesImported)
}
}
refreshMessages()
}
}
} catch (e: Exception) {
context.showErrorToast(e)
messagesFailed++
}
callback.invoke(
when {
messagesImported == 0 -> IMPORT_FAIL
messagesFailed > 0 -> IMPORT_PARTIAL
else -> IMPORT_OK
}
)
}
}
}

View file

@ -0,0 +1,235 @@
package com.simplemobiletools.smsmessenger.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.simplemobiletools.commons.extensions.*
import com.simplemobiletools.commons.helpers.isQPlus
import com.simplemobiletools.commons.helpers.isRPlus
import com.simplemobiletools.smsmessenger.models.MmsAddress
import com.simplemobiletools.smsmessenger.models.MmsBackup
import com.simplemobiletools.smsmessenger.models.MmsPart
import com.simplemobiletools.smsmessenger.models.SmsBackup
import java.io.IOException
import java.io.InputStream
class MessagesReader(private val context: Context) {
fun forEachSms(threadId: Long, block: (SmsBackup) -> Unit) {
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 selectionArgs = arrayOf(threadId.toString())
context.queryCursor(Sms.CONTENT_URI, projection, selection, selectionArgs) { 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)
block(SmsBackup(subscriptionId, address, body, date, dateSent, locked, protocol, read, status, type, serviceCenter))
}
}
// all mms from simple sms are non-text messages
fun forEachMms(threadId: Long, includeTextOnlyAttachment: Boolean = false, block: (MmsBackup) -> Unit) {
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 selectionArgs = if (includeTextOnlyAttachment) {
arrayOf(threadId.toString(), "1")
} else {
arrayOf(threadId.toString())
}
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)
block(
MmsBackup(
creator,
contentType,
deliveryReport,
date,
dateSent,
locked,
messageType,
messageBox,
read,
readReport,
seen,
textOnly,
status,
subject,
subjectCharSet,
subscriptionId,
transactionId,
addresses,
parts
)
)
}
}
@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
}
}
}

View file

@ -0,0 +1,155 @@
package com.simplemobiletools.smsmessenger.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 com.simplemobiletools.commons.extensions.getLongValue
import com.simplemobiletools.commons.extensions.queryCursor
import com.simplemobiletools.commons.helpers.isRPlus
import com.simplemobiletools.smsmessenger.models.MmsAddress
import com.simplemobiletools.smsmessenger.models.MmsBackup
import com.simplemobiletools.smsmessenger.models.MmsPart
import com.simplemobiletools.smsmessenger.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
}
}

View file

@ -0,0 +1,10 @@
package com.simplemobiletools.smsmessenger.models
import com.google.gson.annotations.SerializedName
data class ExportedMessage(
@SerializedName("sms")
val sms: List<SmsBackup>?,
@SerializedName("mms")
val mms: List<MmsBackup>?,
)

View file

@ -0,0 +1,26 @@
package com.simplemobiletools.smsmessenger.models
import android.content.ContentValues
import android.provider.Telephony
import androidx.core.content.contentValuesOf
import com.google.gson.annotations.SerializedName
data class MmsAddress(
@SerializedName("address")
val address: String,
@SerializedName("type")
val type: Int,
@SerializedName("charset")
val charset: Int
) {
fun toContentValues(): ContentValues {
// msgId would be added at the point of insertion
// because it may have changed
return contentValuesOf(
Telephony.Mms.Addr.ADDRESS to address,
Telephony.Mms.Addr.TYPE to type,
Telephony.Mms.Addr.CHARSET to charset,
)
}
}

View file

@ -0,0 +1,69 @@
package com.simplemobiletools.smsmessenger.models
import android.content.ContentValues
import android.provider.Telephony
import androidx.core.content.contentValuesOf
import com.google.gson.annotations.SerializedName
data class MmsBackup(
@SerializedName("creator")
val creator: String?,
@SerializedName("ct_t")
val contentType: String?,
@SerializedName("d_rpt")
val deliveryReport: Int,
@SerializedName("date")
val date: Long,
@SerializedName("date_sent")
val dateSent: Long,
@SerializedName("locked")
val locked: Int,
@SerializedName("m_type")
val messageType: Int,
@SerializedName("msg_box")
val messageBox: Int,
@SerializedName("read")
val read: Int,
@SerializedName("rr")
val readReport: Int,
@SerializedName("seen")
val seen: Int,
@SerializedName("text_only")
val textOnly: Int,
@SerializedName("st")
val status: String?,
@SerializedName("sub")
val subject: String?,
@SerializedName("sub_cs")
val subjectCharSet: String?,
@SerializedName("sub_id")
val subscriptionId: Long,
@SerializedName("tr_id")
val transactionId: String?,
@SerializedName("addresses")
val addresses: List<MmsAddress>,
@SerializedName("parts")
val parts: List<MmsPart>,
) {
fun toContentValues(): ContentValues {
return contentValuesOf(
Telephony.Mms.TRANSACTION_ID to transactionId,
Telephony.Mms.SUBSCRIPTION_ID to subscriptionId,
Telephony.Mms.SUBJECT to subject,
Telephony.Mms.DATE to date,
Telephony.Mms.DATE_SENT to dateSent,
Telephony.Mms.LOCKED to locked,
Telephony.Mms.READ to read,
Telephony.Mms.STATUS to status,
Telephony.Mms.SUBJECT_CHARSET to subjectCharSet,
Telephony.Mms.SEEN to seen,
Telephony.Mms.MESSAGE_TYPE to messageType,
Telephony.Mms.MESSAGE_BOX to messageBox,
Telephony.Mms.DELIVERY_REPORT to deliveryReport,
Telephony.Mms.READ_REPORT to readReport,
Telephony.Mms.CONTENT_TYPE to contentType,
Telephony.Mms.TEXT_ONLY to textOnly,
)
}
}

View file

@ -0,0 +1,54 @@
package com.simplemobiletools.smsmessenger.models
import android.content.ContentValues
import android.provider.Telephony
import androidx.core.content.contentValuesOf
import com.google.gson.annotations.SerializedName
data class MmsPart(
@SerializedName("cd")
val contentDisposition: String?,
@SerializedName("chset")
val charset: String?,
@SerializedName("cid")
val contentId: String?,
@SerializedName("cl")
val contentLocation: String?,
@SerializedName("ct")
val contentType: String,
@SerializedName("ctt_s")
val ctStart: String?,
@SerializedName("ctt_t")
val ctType: String?,
@SerializedName("fn")
val filename: String?,
@SerializedName("name")
val name: String?,
@SerializedName("seq")
val sequenceOrder: Int,
@SerializedName("text")
val text: String?,
@SerializedName("data")
val data: String?,
) {
fun toContentValues(): ContentValues {
return contentValuesOf(
Telephony.Mms.Part.CONTENT_DISPOSITION to contentDisposition,
Telephony.Mms.Part.CHARSET to charset,
Telephony.Mms.Part.CONTENT_ID to contentId,
Telephony.Mms.Part.CONTENT_LOCATION to contentLocation,
Telephony.Mms.Part.CONTENT_TYPE to contentType,
Telephony.Mms.Part.CT_START to ctStart,
Telephony.Mms.Part.CT_TYPE to ctType,
Telephony.Mms.Part.FILENAME to filename,
Telephony.Mms.Part.NAME to name,
Telephony.Mms.Part.SEQ to sequenceOrder,
Telephony.Mms.Part.TEXT to text,
)
}
fun isNonText(): Boolean {
return !(text != null || contentType.lowercase().startsWith("text") || contentType.lowercase() == "application/smil")
}
}

View file

@ -0,0 +1,49 @@
package com.simplemobiletools.smsmessenger.models
import android.content.ContentValues
import android.provider.Telephony
import androidx.core.content.contentValuesOf
import com.google.gson.annotations.SerializedName
data class SmsBackup(
@SerializedName("sub_id")
val subscriptionId: Long,
@SerializedName("address")
val address: String,
@SerializedName("body")
val body: String?,
@SerializedName("date")
val date: Long,
@SerializedName("date_sent")
val dateSent: Long,
@SerializedName("locked")
val locked: Int,
@SerializedName("protocol")
val protocol: String?,
@SerializedName("read")
val read: Int,
@SerializedName("status")
val status: Int,
@SerializedName("type")
val type: Int,
@SerializedName("service_center")
val serviceCenter: String?
) {
fun toContentValues(): ContentValues {
return contentValuesOf(
Telephony.Sms.SUBSCRIPTION_ID to subscriptionId,
Telephony.Sms.ADDRESS to address,
Telephony.Sms.BODY to body,
Telephony.Sms.DATE to date,
Telephony.Sms.DATE_SENT to dateSent,
Telephony.Sms.LOCKED to locked,
Telephony.Sms.PROTOCOL to protocol,
Telephony.Sms.READ to read,
Telephony.Sms.STATUS to status,
Telephony.Sms.TYPE to type,
Telephony.Sms.SERVICE_CENTER to serviceCenter,
)
}
}