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

@ -56,7 +56,7 @@ android {
}
dependencies {
implementation 'com.github.SimpleMobileTools:Simple-Commons:307e9e2e82'
implementation 'com.github.SimpleMobileTools:Simple-Commons:d4b6547969'
implementation 'org.greenrobot:eventbus:3.2.0'
implementation 'com.klinkerapps:android-smsmms:5.2.6'
implementation 'com.github.tibbi:IndicatorFastScroll:c3de1d040a'

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,
)
}
}

View File

@ -0,0 +1,70 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/export_messages_scrollview"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<LinearLayout
android:id="@+id/export_messages_holder"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingStart="@dimen/activity_margin"
android:paddingTop="@dimen/activity_margin"
android:paddingEnd="@dimen/activity_margin">
<com.simplemobiletools.commons.views.MyTextView
android:id="@+id/export_messages_folder_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/folder"
android:textSize="@dimen/smaller_text_size" />
<com.simplemobiletools.commons.views.MyTextView
android:id="@+id/export_messages_folder"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/activity_margin"
android:layout_marginBottom="@dimen/activity_margin"
android:paddingTop="@dimen/small_margin"
android:paddingEnd="@dimen/small_margin"
android:paddingBottom="@dimen/small_margin" />
<com.simplemobiletools.commons.views.MyTextView
android:id="@+id/export_messages_filename_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/small_margin"
android:text="@string/filename_without_json"
android:textSize="@dimen/smaller_text_size" />
<com.simplemobiletools.commons.views.MyEditText
android:id="@+id/export_messages_filename"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/small_margin"
android:layout_marginBottom="@dimen/activity_margin"
android:paddingTop="@dimen/normal_margin"
android:paddingEnd="@dimen/small_margin"
android:textSize="@dimen/normal_text_size" />
<com.simplemobiletools.commons.views.MyAppCompatCheckbox
android:id="@+id/export_sms_checkbox"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="@dimen/activity_margin"
android:paddingTop="@dimen/small_margin"
android:paddingBottom="@dimen/small_margin"
android:text="@string/export_sms" />
<com.simplemobiletools.commons.views.MyAppCompatCheckbox
android:id="@+id/export_mms_checkbox"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="@dimen/activity_margin"
android:paddingTop="@dimen/small_margin"
android:paddingBottom="@dimen/small_margin"
android:text="@string/export_mms" />
</LinearLayout>
</ScrollView>

View File

@ -0,0 +1,35 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/import_messages_scrollview"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<LinearLayout
android:id="@+id/import_messages_holder"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingStart="@dimen/activity_margin"
android:paddingTop="@dimen/activity_margin"
android:paddingEnd="@dimen/activity_margin">
<com.simplemobiletools.commons.views.MyAppCompatCheckbox
android:id="@+id/import_sms_checkbox"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="@dimen/activity_margin"
android:paddingTop="@dimen/small_margin"
android:paddingBottom="@dimen/small_margin"
android:text="@string/import_sms" />
<com.simplemobiletools.commons.views.MyAppCompatCheckbox
android:id="@+id/import_mms_checkbox"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="@dimen/activity_margin"
android:paddingTop="@dimen/small_margin"
android:paddingBottom="@dimen/small_margin"
android:text="@string/import_mms" />
</LinearLayout>
</ScrollView>

View File

@ -26,6 +26,24 @@
android:layout_marginStart="@dimen/normal_margin"
android:layout_marginEnd="@dimen/normal_margin" />
<RelativeLayout
android:id="@+id/pin_indicator_l"
android:layout_width="@dimen/normal_icon_size"
android:layout_height="@dimen/normal_icon_size"
android:layout_alignTop="@+id/conversation_address"
android:layout_alignBottom="@+id/conversation_date"
android:layout_centerVertical="true"
android:layout_marginStart="@dimen/normal_margin"
android:layout_marginEnd="@dimen/normal_margin">
<ImageView
android:id="@+id/pin_indicator"
android:layout_width="@dimen/pin_icon_size"
android:layout_height="@dimen/pin_icon_size"
android:layout_alignParentRight="true"
android:src="@drawable/ic_pin" />
</RelativeLayout>
<TextView
android:id="@+id/conversation_address"
android:layout_width="match_parent"

View File

@ -33,6 +33,16 @@
android:id="@+id/cab_mark_as_unread"
android:title="@string/mark_as_unread"
app:showAsAction="never" />
<item
android:id="@+id/cab_pin_conversation"
android:icon="@drawable/ic_pin"
android:title="@string/pin_conversation"
app:showAsAction="ifRoom" />
<item
android:id="@+id/cab_unpin_conversation"
android:icon="@drawable/ic_unpin"
android:title="@string/unpin_conversation"
app:showAsAction="ifRoom" />
<item
android:id="@+id/cab_select_all"
android:title="@string/select_all"

View File

@ -6,6 +6,14 @@
android:icon="@drawable/ic_search_vector"
android:title="@string/search"
app:showAsAction="always" />
<item
android:id="@+id/import_messages"
android:title="@string/import_messages"
app:showAsAction="never" />
<item
android:id="@+id/export_messages"
android:title="@string/export_messages"
app:showAsAction="never" />
<item
android:id="@+id/settings"
android:title="@string/settings"

View File

@ -19,4 +19,12 @@
android:id="@+id/mark_as_unread"
android:title="@string/mark_as_unread"
app:showAsAction="never" />
<item
android:id="@+id/pin_conversation"
android:title="@string/pin_conversation"
app:showAsAction="never" />
<item
android:id="@+id/unpin_conversation"
android:title="@string/unpin_conversation"
app:showAsAction="never" />
</menu>

View File

@ -15,8 +15,8 @@
<string name="no_reply_support">Odesílatel nepodporuje odpovědi</string>
<string name="draft">Návrh</string>
<string name="sending">Odesílá se…</string>
<string name="export_messages">Export zpráv</string>
<string name="import_messages">Import zpráv</string>
<string name="pin_conversation">Pin to the top</string>
<string name="unpin_conversation">Unpin</string>
<!-- New conversation -->
<string name="new_conversation">Nová konverzace</string>
@ -53,6 +53,17 @@
<string name="mms_file_size_limit">Resize sent MMS images</string>
<string name="mms_file_size_limit_none">No limit</string>
<!-- Export / Import -->
<string name="export_messages">Export zpráv</string>
<string name="messages">Messages</string>
<string name="export_sms">Export SMS</string>
<string name="export_mms">Export MMS</string>
<string name="export_unchecked_error_message">Check at least one of Export SMS or Export MMS</string>
<string name="import_messages">Import zpráv</string>
<string name="import_sms">Import SMS</string>
<string name="import_mms">Import MMS</string>
<string name="import_unchecked_error_message">Check at least one of Import SMS or Import MMS</string>
<!-- FAQ -->
<string name="faq_1_title">Proč aplikace vyžaduje přístup k internetu?</string>
<string name="faq_1_text">Je smutné, že je to nutné pro odesílání příloh MMS. Nebýt schopen posílat MMS by byla opravdu obrovská nevýhoda ve srovnání s jinými aplikacemi, proto jsme se rozhodli jít touto cestou.

View File

@ -15,8 +15,8 @@
<string name="no_reply_support">Afsender understøtter ikke svar</string>
<string name="draft">Udkast</string>
<string name="sending">Sender…</string>
<string name="export_messages">Eksporter beskeder</string>
<string name="import_messages">Importer beskeder</string>
<string name="pin_conversation">Pin to the top</string>
<string name="unpin_conversation">Unpin</string>
<!-- New conversation -->
<string name="new_conversation">Ny Samtale</string>
@ -53,6 +53,17 @@
<string name="mms_file_size_limit">Resize sent MMS images</string>
<string name="mms_file_size_limit_none">No limit</string>
<!-- Export / Import -->
<string name="messages">Messages</string>
<string name="export_messages">Eksporter beskeder</string>
<string name="export_sms">Export SMS</string>
<string name="export_mms">Export MMS</string>
<string name="export_unchecked_error_message">Check at least one of Export SMS or Export MMS</string>
<string name="import_messages">Importer beskeder</string>
<string name="import_sms">Import SMS</string>
<string name="import_mms">Import MMS</string>
<string name="import_unchecked_error_message">Check at least one of Import SMS or Import MMS</string>
<!-- FAQ -->
<string name="faq_1_title">Hvorfor kræver appen adgang til internettet?</string>
<string name="faq_1_text">Desværre er det nødvendigt for at sende MMS-vedhæftede filer. Ikke at kunne være i stand til at sende MMS ville være en virkelig stor ulempe i forhold til andre apps, så vi besluttede at gå denne vej.

View File

@ -15,8 +15,8 @@
<string name="no_reply_support">Der Absender unterstützt keine Antworten.</string>
<string name="draft">Entwurf</string>
<string name="sending">Sende…</string>
<string name="export_messages">Export messages</string>
<string name="import_messages">Import messages</string>
<string name="pin_conversation">Pin to the top</string>
<string name="unpin_conversation">Unpin</string>
<!-- New conversation -->
<string name="new_conversation">Neuer Chat</string>
@ -53,6 +53,17 @@
<string name="mms_file_size_limit">Resize sent MMS images</string>
<string name="mms_file_size_limit_none">No limit</string>
<!-- Export / Import -->
<string name="messages">Messages</string>
<string name="export_messages">Export messages</string>
<string name="export_sms">Export SMS</string>
<string name="export_mms">Export MMS</string>
<string name="export_unchecked_error_message">Check at least one of Export SMS or Export MMS</string>
<string name="import_messages">Import messages</string>
<string name="import_sms">Import SMS</string>
<string name="import_mms">Import MMS</string>
<string name="import_unchecked_error_message">Check at least one of Import SMS or Import MMS</string>
<!-- FAQ -->
<string name="faq_1_title">Warum benötigt diese App Internetzugriff?</string>
<string name="faq_1_text">Leider ist dies nötig, um MMS-Anhänge zu versenden. Es wäre ein großer Nachteil gegenüber anderen Apps, wenn keine MMS versendet werden könnten, also haben wir uns für diesen Weg entschieden.

View File

@ -15,8 +15,8 @@
<string name="no_reply_support">Ο αποστολέας δεν υποστηρίζει απαντήσεις</string>
<string name="draft">Πρόχειρο</string>
<string name="sending">Γίνεται αποστολή…</string>
<string name="export_messages">Εξαγωγή μηνυμάτων</string>
<string name="import_messages">Εισαγωγή μηνυμάτων</string>
<string name="pin_conversation">Pin to the top</string>
<string name="unpin_conversation">Unpin</string>
<!-- New conversation -->
<string name="new_conversation">Νέα συνομιλία</string>
@ -53,6 +53,17 @@
<string name="mms_file_size_limit">Αλλαγή μεγέθους απεσταλμένων εικόνων MMS</string>
<string name="mms_file_size_limit_none">Χωρίς όριο</string>
<!-- Export / Import -->
<string name="messages">Messages</string>
<string name="export_messages">Εξαγωγή μηνυμάτων</string>
<string name="export_sms">Export SMS</string>
<string name="export_mms">Export MMS</string>
<string name="export_unchecked_error_message">Check at least one of Export SMS or Export MMS</string>
<string name="import_messages">Εισαγωγή μηνυμάτων</string>
<string name="import_sms">Import SMS</string>
<string name="import_mms">Import MMS</string>
<string name="import_unchecked_error_message">Check at least one of Import SMS or Import MMS</string>
<!-- FAQ -->
<string name="faq_1_title">Γιατί η εφαρμογή απαιτεί πρόσβαση στο Internet;</string>
<string name="faq_1_text">Δυστυχώς, απαιτείται για την αποστολή συνημμένων MMS. Το να μην είμαστε σε θέση να στείλουμε MMS θα αποτελούσε πραγματικά τεράστιο μειονέκτημα σε σύγκριση με άλλες εφαρμογές, επομένως αποφασίσαμε να ακολουθήσουμε αυτόν τον δρόμο.

View File

@ -15,8 +15,8 @@
<string name="no_reply_support">Sender doesn\'t support replies</string>
<string name="draft">Draft</string>
<string name="sending">Sending…</string>
<string name="export_messages">Export messages</string>
<string name="import_messages">Import messages</string>
<string name="pin_conversation">Pin to the top</string>
<string name="unpin_conversation">Unpin</string>
<!-- New conversation -->
<string name="new_conversation">Nueva conversación</string>
@ -53,6 +53,17 @@
<string name="mms_file_size_limit">Resize sent MMS images</string>
<string name="mms_file_size_limit_none">No limit</string>
<!-- Export / Import -->
<string name="messages">Messages</string>
<string name="export_messages">Export messages</string>
<string name="export_sms">Export SMS</string>
<string name="export_mms">Export MMS</string>
<string name="export_unchecked_error_message">Check at least one of Export SMS or Export MMS</string>
<string name="import_messages">Import messages</string>
<string name="import_sms">Import SMS</string>
<string name="import_mms">Import MMS</string>
<string name="import_unchecked_error_message">Check at least one of Import SMS or Import MMS</string>
<!-- FAQ -->
<string name="faq_1_title">¿Por qué la aplicación requiere acceso a internet?</string>
<string name="faq_1_text">Tristemente es necesitado para enviar archivos adjuntos MMS. El no poder enviar MMS sería una desventaja realmente enorme comparada con otras aplicaciones, así que decidimos tomar este camino.

View File

@ -15,8 +15,8 @@
<string name="no_reply_support">Lähettäjä ei tue vastauksia</string>
<string name="draft">Luonnos</string>
<string name="sending">Lähetetään…</string>
<string name="export_messages">Export messages</string>
<string name="import_messages">Import messages</string>
<string name="pin_conversation">Pin to the top</string>
<string name="unpin_conversation">Unpin</string>
<!-- New conversation -->
<string name="new_conversation">Uusi keskustelu</string>
@ -53,6 +53,17 @@
<string name="mms_file_size_limit">Resize sent MMS images</string>
<string name="mms_file_size_limit_none">No limit</string>
<!-- Export / Import -->
<string name="messages">Messages</string>
<string name="export_messages">Export messages</string>
<string name="export_sms">Export SMS</string>
<string name="export_mms">Export MMS</string>
<string name="export_unchecked_error_message">Check at least one of Export SMS or Export MMS</string>
<string name="import_messages">Import messages</string>
<string name="import_sms">Import SMS</string>
<string name="import_mms">Import MMS</string>
<string name="import_unchecked_error_message">Check at least one of Import SMS or Import MMS</string>
<!-- FAQ -->
<string name="faq_1_title">Miksi sovellus vaatii Internet-yhteyden?</string>
<string name="faq_1_text">Valitettavasti sitä tarvitaan multimediaviestin-liitteiden lähettämiseen. Multimediaviestien lähettämättä jättäminen olisi todella valtava haitta muihin sovelluksiin verrattuna, joten päätimme mennä tällä tavalla.

View File

@ -15,8 +15,8 @@
<string name="no_reply_support">L\'expéditeur n\'accepte pas les réponses</string>
<string name="draft">Brouillon</string>
<string name="sending">Envoi en cours…</string>
<string name="export_messages">Export de messages</string>
<string name="import_messages">Import de messages</string>
<string name="pin_conversation">Pin to the top</string>
<string name="unpin_conversation">Unpin</string>
<!-- New conversation -->
<string name="new_conversation">Nouvelle conversation</string>
@ -53,6 +53,17 @@
<string name="mms_file_size_limit">Redimensionner les images MMS envoyées</string>
<string name="mms_file_size_limit_none">Pas de limite</string>
<!-- Export / Import -->
<string name="messages">Messages</string>
<string name="export_messages">Export de messages</string>
<string name="export_sms">Export SMS</string>
<string name="export_mms">Export MMS</string>
<string name="export_unchecked_error_message">Check at least one of Export SMS or Export MMS</string>
<string name="import_messages">Import de messages</string>
<string name="import_sms">Import SMS</string>
<string name="import_mms">Import MMS</string>
<string name="import_unchecked_error_message">Check at least one of Import SMS or Import MMS</string>
<!-- FAQ -->
<string name="faq_1_title">Pourquoi cette application a besoin d\'un accès à internet ?</string>
<string name="faq_1_text">Malheureusement, cela est nécessaire pour envoyer des pièces jointes dans les MMS. Ne pas pouvoir envoyer de MMS serait un énorme désavantage comparé à d\'autres applications, nous avons donc décidé de faire ainsi.

View File

@ -15,8 +15,8 @@
<string name="no_reply_support">Sender doesn\'t support replies</string>
<string name="draft">Draft</string>
<string name="sending">Sending…</string>
<string name="export_messages">Export messages</string>
<string name="import_messages">Import messages</string>
<string name="pin_conversation">Pin to the top</string>
<string name="unpin_conversation">Unpin</string>
<!-- New conversation -->
<string name="new_conversation">Nova conversa</string>
@ -53,6 +53,17 @@
<string name="mms_file_size_limit">Resize sent MMS images</string>
<string name="mms_file_size_limit_none">No limit</string>
<!-- Export / Import -->
<string name="messages">Messages</string>
<string name="export_messages">Export messages</string>
<string name="export_sms">Export SMS</string>
<string name="export_mms">Export MMS</string>
<string name="export_unchecked_error_message">Check at least one of Export SMS or Export MMS</string>
<string name="import_messages">Import messages</string>
<string name="import_sms">Import SMS</string>
<string name="import_mms">Import MMS</string>
<string name="import_unchecked_error_message">Check at least one of Import SMS or Import MMS</string>
<!-- FAQ -->
<string name="faq_1_title">Por que o aplicativo necesita acceder a Internet?</string>
<string name="faq_1_text">Infelizmente é a única forma para poder enviar anexos MMS. A incapacidade de non conseguir enviar MMS sería unha enorme desvantaxe comparativamente a outros aplicativos e, por iso, tomamos esta decisión. Pero, como habitualmente, o aplicativo non ten anuncios, non rastrea os utilizadores nin recolle datos persoais. Este permiso só é necesario para enviar as MMS.</string>

View File

@ -15,8 +15,8 @@
<string name="no_reply_support">Sender doesn\'t support replies</string>
<string name="draft">Draft</string>
<string name="sending">Sending…</string>
<string name="export_messages">Export messages</string>
<string name="import_messages">Import messages</string>
<string name="pin_conversation">Pin to the top</string>
<string name="unpin_conversation">Unpin</string>
<!-- New conversation -->
<string name="new_conversation">Percakapan baru</string>
@ -53,6 +53,17 @@
<string name="mms_file_size_limit">Resize sent MMS images</string>
<string name="mms_file_size_limit_none">No limit</string>
<!-- Export / Import -->
<string name="messages">Messages</string>
<string name="export_messages">Export messages</string>
<string name="export_sms">Export SMS</string>
<string name="export_mms">Export MMS</string>
<string name="export_unchecked_error_message">Check at least one of Export SMS or Export MMS</string>
<string name="import_messages">Import messages</string>
<string name="import_sms">Import SMS</string>
<string name="import_mms">Import MMS</string>
<string name="import_unchecked_error_message">Check at least one of Import SMS or Import MMS</string>
<!-- FAQ -->
<string name="faq_1_title">Mengapa aplikasi membutuhkan akses ke internet?</string>
<string name="faq_1_text">Sayangnya itu diperlukan untuk mengirim lampiran MMS. Tidak dapat mengirim MMS akan menjadi kerugian yang sangat besar dibandingkan dengan aplikasi lain, jadi kami memutuskan untuk menggunakan cara ini.

View File

@ -15,8 +15,8 @@
<string name="no_reply_support">Il mittente non accetta risposte</string>
<string name="draft">Bozza</string>
<string name="sending">Invio…</string>
<string name="export_messages">Export messages</string>
<string name="import_messages">Import messages</string>
<string name="pin_conversation">Pin to the top</string>
<string name="unpin_conversation">Unpin</string>
<!-- New conversation -->
<string name="new_conversation">Nuova conversazione</string>
@ -53,6 +53,17 @@
<string name="mms_file_size_limit">Resize sent MMS images</string>
<string name="mms_file_size_limit_none">No limit</string>
<!-- Export / Import -->
<string name="messages">Messages</string>
<string name="export_messages">Export messages</string>
<string name="export_sms">Export SMS</string>
<string name="export_mms">Export MMS</string>
<string name="export_unchecked_error_message">Check at least one of Export SMS or Export MMS</string>
<string name="import_messages">Import messages</string>
<string name="import_sms">Import SMS</string>
<string name="import_mms">Import MMS</string>
<string name="import_unchecked_error_message">Check at least one of Import SMS or Import MMS</string>
<!-- FAQ -->
<string name="faq_1_title">Perché l\'applicazione richiede l\'accesso ad internet?</string>
<string name="faq_1_text">Purtroppo è necessario per poter inviare gli allegati degli MMS. Non essere in grado di inviare gli MMS sarebbe un grosso svantaggio in confronto ad altre applicazioni, quindi abbiamo deciso di intraprendere questa strada.

View File

@ -15,8 +15,8 @@
<string name="no_reply_support">Sender doesn\'t support replies</string>
<string name="draft">Draft</string>
<string name="sending">Sending…</string>
<string name="export_messages">Export messages</string>
<string name="import_messages">Import messages</string>
<string name="pin_conversation">Pin to the top</string>
<string name="unpin_conversation">Unpin</string>
<!-- New conversation -->
<string name="new_conversation">新しい会話</string>
@ -53,6 +53,17 @@
<string name="mms_file_size_limit">Resize sent MMS images</string>
<string name="mms_file_size_limit_none">No limit</string>
<!-- Export / Import -->
<string name="messages">Messages</string>
<string name="export_messages">Export messages</string>
<string name="export_sms">Export SMS</string>
<string name="export_mms">Export MMS</string>
<string name="export_unchecked_error_message">Check at least one of Export SMS or Export MMS</string>
<string name="import_messages">Import messages</string>
<string name="import_sms">Import SMS</string>
<string name="import_mms">Import MMS</string>
<string name="import_unchecked_error_message">Check at least one of Import SMS or Import MMS</string>
<!-- FAQ -->
<string name="faq_1_title">なぜアプリ使用にインターネットへのアクセスが必要なのですか?</string>
<string name="faq_1_text">生憎、MMS(マルチメディアメッセージサービス)にインターネットが必要となります。他のアプリと比較して、MMSを使用出来ないと大きな損になるので、こうすることに決めました。

View File

@ -15,8 +15,8 @@
<string name="no_reply_support">Sender doesn\'t support replies</string>
<string name="draft">Draft</string>
<string name="sending">Sending…</string>
<string name="export_messages">Export messages</string>
<string name="import_messages">Import messages</string>
<string name="pin_conversation">Pin to the top</string>
<string name="unpin_conversation">Unpin</string>
<!-- New conversation -->
<string name="new_conversation">Naujas pokalbis</string>
@ -53,6 +53,17 @@
<string name="mms_file_size_limit">Resize sent MMS images</string>
<string name="mms_file_size_limit_none">No limit</string>
<!-- Export / Import -->
<string name="messages">Messages</string>
<string name="export_messages">Export messages</string>
<string name="export_sms">Export SMS</string>
<string name="export_mms">Export MMS</string>
<string name="export_unchecked_error_message">Check at least one of Export SMS or Export MMS</string>
<string name="import_messages">Import messages</string>
<string name="import_sms">Import SMS</string>
<string name="import_mms">Import MMS</string>
<string name="import_unchecked_error_message">Check at least one of Import SMS or Import MMS</string>
<!-- FAQ -->
<string name="faq_1_title">Why does the app require access to the internet?</string>
<string name="faq_1_text">Sadly it is needed for sending MMS attachments. Not being able to send MMS would be a really huge disadvantage compared to other apps, so we decided to go this way.

View File

@ -15,8 +15,8 @@
<string name="no_reply_support">Sender doesn\'t support replies</string>
<string name="draft">Draft</string>
<string name="sending">Sending…</string>
<string name="export_messages">Export messages</string>
<string name="import_messages">Import messages</string>
<string name="pin_conversation">Pin to the top</string>
<string name="unpin_conversation">Unpin</string>
<!-- New conversation -->
<string name="new_conversation">പുതിയ സംഭാഷണം</string>
@ -53,6 +53,17 @@
<string name="mms_file_size_limit">Resize sent MMS images</string>
<string name="mms_file_size_limit_none">No limit</string>
<!-- Export / Import -->
<string name="messages">Messages</string>
<string name="export_messages">Export messages</string>
<string name="export_sms">Export SMS</string>
<string name="export_mms">Export MMS</string>
<string name="export_unchecked_error_message">Check at least one of Export SMS or Export MMS</string>
<string name="import_messages">Import messages</string>
<string name="import_sms">Import SMS</string>
<string name="import_mms">Import MMS</string>
<string name="import_unchecked_error_message">Check at least one of Import SMS or Import MMS</string>
<!-- FAQ -->
<string name="faq_1_title">അപ്ലിക്കേഷന് ഇന്റർനെറ്റിലേക്ക് ആവശ്യമായി വരുന്നത് എന്തുകൊണ്ട്?</string>
<string name="faq_1_text">നിർഭാഗ്യവശാൽ, MMS അറ്റാച്ചുമെന്റുകൾ അയക്കുന്നതിനു ഇത് ആവശ്യമാണ്. മറ്റ് ആപ്ലിക്കേഷനുകളുമായി താരതമ്യപ്പെടുത്തുമ്പോൾ MMS അയയ്ക്കാൻ കഴിയുന്നില്ല എന്നത് ഒരു വലിയ പോരായ്മയാണ്, അതിനാൽ ഞങ്ങൾ ഈ റൂട്ടിൽ പോകാൻ തീരുമാനിച്ചു.

View File

@ -15,8 +15,8 @@
<string name="no_reply_support">Afzender ondersteunt geen antwoorden</string>
<string name="draft">Concept</string>
<string name="sending">Versturen…</string>
<string name="export_messages">Berichten exporteren</string>
<string name="import_messages">Berichten importeren</string>
<string name="pin_conversation">Pin to the top</string>
<string name="unpin_conversation">Unpin</string>
<!-- New conversation -->
<string name="new_conversation">Nieuw gesprek</string>
@ -53,6 +53,17 @@
<string name="mms_file_size_limit">Afbeelding verkleinen voor MMS</string>
<string name="mms_file_size_limit_none">Geen limiet</string>
<!-- Export / Import -->
<string name="messages">Messages</string>
<string name="export_messages">Berichten exporteren</string>
<string name="export_sms">Export SMS</string>
<string name="export_mms">Export MMS</string>
<string name="export_unchecked_error_message">Check at least one of Export SMS or Export MMS</string>
<string name="import_messages">Berichten importeren</string>
<string name="import_sms">Import SMS</string>
<string name="import_mms">Import MMS</string>
<string name="import_unchecked_error_message">Check at least one of Import SMS or Import MMS</string>
<!-- FAQ -->
<string name="faq_1_title">Waarom heeft deze app toegang nodig tot het internet?</string>
<string name="faq_1_text">Dit is helaas nodig voor het verzenden van MMS-bijlagen. Het versturen van MMS-berichten onmogelijk maken zou een te groot nadeel t.o.v. andere apps betekenen en daarom hebben we besloten om het toch toe te voegen.

View File

@ -15,8 +15,8 @@
<string name="no_reply_support">Nadawca nie obsługuje odpowiedzi</string>
<string name="draft">Szkic</string>
<string name="sending">Wysyłanie…</string>
<string name="export_messages">Eksportuj wiadomości</string>
<string name="import_messages">Importuj wiadomości</string>
<string name="pin_conversation">Przypnij na górze</string>
<string name="unpin_conversation">Odepnij</string>
<!-- New conversation -->
<string name="new_conversation">Nowa rozmowa</string>
@ -55,6 +55,17 @@
<string name="mms_file_size_limit">Rozmiar wysyłanych obrazków w MMS-ach</string>
<string name="mms_file_size_limit_none">Bez limitu</string>
<!-- Export / Import -->
<string name="messages">Messages</string>
<string name="export_messages">Eksportuj wiadomości</string>
<string name="export_sms">Export SMS</string>
<string name="export_mms">Export MMS</string>
<string name="export_unchecked_error_message">Check at least one of Export SMS or Export MMS</string>
<string name="import_messages">Importuj wiadomości</string>
<string name="import_sms">Import SMS</string>
<string name="import_mms">Import MMS</string>
<string name="import_unchecked_error_message">Check at least one of Import SMS or Import MMS</string>
<!-- FAQ -->
<string name="faq_1_title">Dlaczego aplikacja wymaga dostępu do Internetu?</string>
<string name="faq_1_text">Niestety jest to konieczne do wysyłania załączników MMS. Brak możliwości wysyłania MMS-ów byłby naprawdę ogromną wadą w porównaniu z innymi aplikacjami, więc zdecydowaliśmy się pójść tą drogą.

View File

@ -15,8 +15,8 @@
<string name="no_reply_support">O remetente não aceita respostas</string>
<string name="draft">Rascunho</string>
<string name="sending">A enviar…</string>
<string name="export_messages">Exportar mensagens</string>
<string name="import_messages">Importar mensagens</string>
<string name="pin_conversation">Pin to the top</string>
<string name="unpin_conversation">Unpin</string>
<!-- New conversation -->
<string name="new_conversation">Nova conversa</string>
@ -53,6 +53,17 @@
<string name="mms_file_size_limit">Resize sent MMS images</string>
<string name="mms_file_size_limit_none">No limit</string>
<!-- Export / Import -->
<string name="messages">Messages</string>
<string name="export_messages">Exportar mensagens</string>
<string name="export_sms">Export SMS</string>
<string name="export_mms">Export MMS</string>
<string name="export_unchecked_error_message">Check at least one of Export SMS or Export MMS</string>
<string name="import_messages">Importar mensagens</string>
<string name="import_sms">Import SMS</string>
<string name="import_mms">Import MMS</string>
<string name="import_unchecked_error_message">Check at least one of Import SMS or Import MMS</string>
<!-- FAQ -->
<string name="faq_1_title">Porque é que a aplicação necessita de aceder à Internet?</string>
<string name="faq_1_text">Infelizmente é a única forma para poder enviar anexos MMS. A incapacidade de não conseguir enviar MMS seria uma enorme desvantagem comparativamente a outras aplicações e, por isso, tomámos esta decisão.

View File

@ -15,8 +15,8 @@
<string name="no_reply_support">Отправитель не поддерживает ответы</string>
<string name="draft">Черновик</string>
<string name="sending">Отправка…</string>
<string name="export_messages">Экспорт сообщений</string>
<string name="import_messages">Импорт сообщений</string>
<string name="pin_conversation">Pin to the top</string>
<string name="unpin_conversation">Unpin</string>
<!-- New conversation -->
<string name="new_conversation">Новая переписка</string>
@ -55,6 +55,17 @@
<string name="mms_file_size_limit">Изменять размер отправляемых в MMS изображений</string>
<string name="mms_file_size_limit_none">Нет ограничения</string>
<!-- Export / Import -->
<string name="messages">Messages</string>
<string name="export_messages">Экспорт сообщений</string>
<string name="export_sms">Export SMS</string>
<string name="export_mms">Export MMS</string>
<string name="export_unchecked_error_message">Check at least one of Export SMS or Export MMS</string>
<string name="import_messages">Импорт сообщений</string>
<string name="import_sms">Import SMS</string>
<string name="import_mms">Import MMS</string>
<string name="import_unchecked_error_message">Check at least one of Import SMS or Import MMS</string>
<!-- FAQ -->
<string name="faq_1_title">Почему приложение требует доступ к интернету?</string>
<string name="faq_1_text">К сожалению, это необходимо для отправки вложений MMS. Отсутствие возможности отправлять MMS-сообщения было бы огромным недостатком нашего приложения по сравнению с другими, поэтому мы решили пойти этим путём.

View File

@ -15,8 +15,8 @@
<string name="no_reply_support">Odosielateľ nepodporuje odpovede</string>
<string name="draft">Koncept</string>
<string name="sending">Odosiela sa…</string>
<string name="export_messages">Exportovať správy</string>
<string name="import_messages">Importovať správy</string>
<string name="pin_conversation">Pripnúť na vrch</string>
<string name="unpin_conversation">Odopnúť</string>
<!-- New conversation -->
<string name="new_conversation">Nová konverzácia</string>
@ -55,6 +55,17 @@
<string name="mms_file_size_limit">Zmenšiť MMS obrázky pri odosielaní</string>
<string name="mms_file_size_limit_none">Žiadny limit</string>
<!-- Export / Import -->
<string name="messages">Správy</string>
<string name="export_messages">Exportovať správy</string>
<string name="export_sms">Exportovať SMS</string>
<string name="export_mms">Exportovať MMS</string>
<string name="export_unchecked_error_message">Označte buď exportovanie SMS alebo exportovanie MMS</string>
<string name="import_messages">Importovať správy</string>
<string name="import_sms">Importovať SMS</string>
<string name="import_mms">Importovať MMS</string>
<string name="import_unchecked_error_message">Označte buď importovanie SMS alebo importovanie MMS</string>
<!-- FAQ -->
<string name="faq_1_title">Prečo vyžaduje apka prístup na internet?</string>
<string name="faq_1_text">Je to žiaľ nevyhnutné pre odosielanie MMS príloh. Ak by sa ich nedalo odosielať, bola by to obrovská nevýhoda v porovnaní s konkurenciou, preto sme sa rozhodli takto.

View File

@ -15,8 +15,8 @@
<string name="no_reply_support">Sender doesn\'t support replies</string>
<string name="draft">Draft</string>
<string name="sending">Sending…</string>
<string name="export_messages">Export messages</string>
<string name="import_messages">Import messages</string>
<string name="pin_conversation">Pin to the top</string>
<string name="unpin_conversation">Unpin</string>
<!-- New conversation -->
<string name="new_conversation">Yeni görüşme</string>
@ -53,6 +53,17 @@
<string name="mms_file_size_limit">Resize sent MMS images</string>
<string name="mms_file_size_limit_none">No limit</string>
<!-- Export / Import -->
<string name="messages">Messages</string>
<string name="export_messages">Export messages</string>
<string name="export_sms">Export SMS</string>
<string name="export_mms">Export MMS</string>
<string name="export_unchecked_error_message">Check at least one of Export SMS or Export MMS</string>
<string name="import_messages">Import messages</string>
<string name="import_sms">Import SMS</string>
<string name="import_mms">Import MMS</string>
<string name="import_unchecked_error_message">Check at least one of Import SMS or Import MMS</string>
<!-- FAQ -->
<string name="faq_1_title">Uygulama neden internete erişim gerektiriyor?</string>
<string name="faq_1_text">Ne yazık ki MMS eklerini göndermek için gerekli. MMS gönderememek, diğer uygulamalara kıyasla gerçekten çok büyük bir dezavantaj olacaktır, bu yüzden bu şekilde gitmeye karar verdik.

View File

@ -15,8 +15,8 @@
<string name="no_reply_support">Sender doesn\'t support replies</string>
<string name="draft">Draft</string>
<string name="sending">Sending…</string>
<string name="export_messages">Export messages</string>
<string name="import_messages">Import messages</string>
<string name="pin_conversation">Pin to the top</string>
<string name="unpin_conversation">Unpin</string>
<!-- New conversation -->
<string name="new_conversation">Нове листування</string>
@ -53,6 +53,17 @@
<string name="mms_file_size_limit">Resize sent MMS images</string>
<string name="mms_file_size_limit_none">No limit</string>
<!-- Export / Import -->
<string name="messages">Messages</string>
<string name="export_messages">Export messages</string>
<string name="export_sms">Export SMS</string>
<string name="export_mms">Export MMS</string>
<string name="export_unchecked_error_message">Check at least one of Export SMS or Export MMS</string>
<string name="import_messages">Import messages</string>
<string name="import_sms">Import SMS</string>
<string name="import_mms">Import MMS</string>
<string name="import_unchecked_error_message">Check at least one of Import SMS or Import MMS</string>
<!-- FAQ -->
<string name="faq_1_title">Чому додаток потрубує доступу до інтернету?</string>
<string name="faq_1_text">Нажаль, це необхідно для відправки вкладень MMS. Неспроможність надсилати MMS-повідомлення була б великим недоліком нашого додатку порівняно з іншими, тому ми так зробили.

View File

@ -15,8 +15,8 @@
<string name="no_reply_support">Sender doesn\'t support replies</string>
<string name="draft">Draft</string>
<string name="sending">Sending…</string>
<string name="export_messages">Export messages</string>
<string name="import_messages">Import messages</string>
<string name="pin_conversation">Pin to the top</string>
<string name="unpin_conversation">Unpin</string>
<!-- New conversation -->
<string name="new_conversation">新的对话</string>
@ -53,6 +53,17 @@
<string name="mms_file_size_limit">Resize sent MMS images</string>
<string name="mms_file_size_limit_none">No limit</string>
<!-- Export / Import -->
<string name="messages">Messages</string>
<string name="export_messages">Export messages</string>
<string name="export_sms">Export SMS</string>
<string name="export_mms">Export MMS</string>
<string name="export_unchecked_error_message">Check at least one of Export SMS or Export MMS</string>
<string name="import_messages">Import messages</string>
<string name="import_sms">Import SMS</string>
<string name="import_mms">Import MMS</string>
<string name="import_unchecked_error_message">Check at least one of Import SMS or Import MMS</string>
<!-- FAQ -->
<string name="faq_1_title">为什么该应用需要访问互联网?</string>
<string name="faq_1_text">很遗憾这对于发送彩信附件是必须的。如果不能发送彩信的话这相比其他应用会是一个巨大的劣势,所以我们决定这么采取现在的方式。

View File

@ -5,4 +5,5 @@
<dimen name="play_outline_size">36dp</dimen>
<dimen name="attachment_preview_size">60dp</dimen>
<dimen name="remove_attachment_size">24dp</dimen>
<dimen name="pin_icon_size">15dp</dimen>
</resources>

View File

@ -15,8 +15,8 @@
<string name="no_reply_support">Sender doesn\'t support replies</string>
<string name="draft">Draft</string>
<string name="sending">Sending…</string>
<string name="export_messages">Export messages</string>
<string name="import_messages">Import messages</string>
<string name="pin_conversation">Pin to the top</string>
<string name="unpin_conversation">Unpin</string>
<!-- New conversation -->
<string name="new_conversation">New conversation</string>
@ -53,6 +53,17 @@
<string name="mms_file_size_limit">Resize sent MMS images</string>
<string name="mms_file_size_limit_none">No limit</string>
<!-- Export / Import -->
<string name="messages">Messages</string>
<string name="export_messages">Export messages</string>
<string name="export_sms">Export SMS</string>
<string name="export_mms">Export MMS</string>
<string name="import_messages">Import messages</string>
<string name="import_sms">Import SMS</string>
<string name="import_mms">Import MMS</string>
<string name="export_unchecked_error_message">Check at least one of Export SMS or Export MMS</string>
<string name="import_unchecked_error_message">Check at least one of Import SMS or Import MMS</string>
<!-- FAQ -->
<string name="faq_1_title">Why does the app require access to the internet?</string>
<string name="faq_1_text">Sadly it is needed for sending MMS attachments. Not being able to send MMS would be a really huge disadvantage compared to other apps, so we decided to go this way.