Merge branch 'master' into fix/wrong-sender-name
This commit is contained in:
commit
7fae2d8324
53 changed files with 1775 additions and 55 deletions
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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()
|
||||
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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>?,
|
||||
)
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue