diff --git a/app/build.gradle b/app/build.gradle index ab02e588..54a67a86 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -56,7 +56,7 @@ android { } dependencies { - implementation 'com.github.SimpleMobileTools:Simple-Commons:f49f7b5f89' + implementation 'com.github.SimpleMobileTools:Simple-Commons:540c8c39ba' implementation 'org.greenrobot:eventbus:3.2.0' implementation 'com.klinkerapps:android-smsmms:5.2.6' implementation 'com.github.tibbi:IndicatorFastScroll:c3de1d040a' diff --git a/app/src/main/kotlin/com/simplemobiletools/smsmessenger/activities/MainActivity.kt b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/activities/MainActivity.kt index dcfea4ea..2a66fa41 100644 --- a/app/src/main/kotlin/com/simplemobiletools/smsmessenger/activities/MainActivity.kt +++ b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/activities/MainActivity.kt @@ -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,15 +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?) { @@ -109,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) } @@ -123,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) } } @@ -323,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() diff --git a/app/src/main/kotlin/com/simplemobiletools/smsmessenger/dialogs/ExportMessagesDialog.kt b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/dialogs/ExportMessagesDialog.kt new file mode 100644 index 00000000..774c1250 --- /dev/null +++ b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/dialogs/ExportMessagesDialog.kt @@ -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) + } + } + } + } + } +} diff --git a/app/src/main/kotlin/com/simplemobiletools/smsmessenger/dialogs/ImportMessagesDialog.kt b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/dialogs/ImportMessagesDialog.kt new file mode 100644 index 00000000..83ef644d --- /dev/null +++ b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/dialogs/ImportMessagesDialog.kt @@ -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 + } + ) + } +} diff --git a/app/src/main/kotlin/com/simplemobiletools/smsmessenger/extensions/Collections.kt b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/extensions/Collections.kt index bc2acca7..38e3d1b2 100644 --- a/app/src/main/kotlin/com/simplemobiletools/smsmessenger/extensions/Collections.kt +++ b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/extensions/Collections.kt @@ -1,5 +1,7 @@ package com.simplemobiletools.smsmessenger.extensions +import android.content.ContentValues + inline fun List.indexOfFirstOrNull(predicate: (T) -> Boolean): Int? { var index = 0 for (item in this) { @@ -9,3 +11,22 @@ inline fun List.indexOfFirstOrNull(predicate: (T) -> Boolean): Int? { } return null } + +fun Map.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 +} diff --git a/app/src/main/kotlin/com/simplemobiletools/smsmessenger/extensions/Context.kt b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/extensions/Context.kt index 4ff626ce..a4c845db 100644 --- a/app/src/main/kotlin/com/simplemobiletools/smsmessenger/extensions/Context.kt +++ b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/extensions/Context.kt @@ -255,6 +255,20 @@ fun Context.getConversations(threadId: Long? = null, privateContacts: ArrayList< return conversations } +fun Context.getConversationIds(): List { + 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() + 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 { diff --git a/app/src/main/kotlin/com/simplemobiletools/smsmessenger/extensions/Cursor.kt b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/extensions/Cursor.kt new file mode 100644 index 00000000..fb4044d6 --- /dev/null +++ b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/extensions/Cursor.kt @@ -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 +} diff --git a/app/src/main/kotlin/com/simplemobiletools/smsmessenger/extensions/gson/Gson.kt b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/extensions/gson/Gson.kt new file mode 100644 index 00000000..4e1dcf2e --- /dev/null +++ b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/extensions/gson/Gson.kt @@ -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>(){}.type, MapDeserializerDoubleAsIntFix()) +val gson : Gson = gsonBuilder.create() + diff --git a/app/src/main/kotlin/com/simplemobiletools/smsmessenger/extensions/gson/JsonElement.kt b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/extensions/gson/JsonElement.kt new file mode 100644 index 00000000..876d6353 --- /dev/null +++ b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/extensions/gson/JsonElement.kt @@ -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 JsonElement.safeConversion(converter: () -> T?): T? { + + return try { + converter() + } catch (e: Exception) { + null + } +} diff --git a/app/src/main/kotlin/com/simplemobiletools/smsmessenger/extensions/gson/JsonObject.kt b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/extensions/gson/JsonObject.kt new file mode 100644 index 00000000..76e7bc8f --- /dev/null +++ b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/extensions/gson/JsonObject.kt @@ -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 + diff --git a/app/src/main/kotlin/com/simplemobiletools/smsmessenger/extensions/gson/MapDeserializerDoubleAsIntFix.kt b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/extensions/gson/MapDeserializerDoubleAsIntFix.kt new file mode 100644 index 00000000..1c08525e --- /dev/null +++ b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/extensions/gson/MapDeserializerDoubleAsIntFix.kt @@ -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?> { + @Throws(JsonParseException::class) + override fun deserialize(json: JsonElement, typeOfT: Type, context: JsonDeserializationContext): Map? { + return read(json) as Map? + } + + fun read(element: JsonElement): Any? { + when { + element.isJsonArray -> { + val list: MutableList = ArrayList() + val arr = element.asJsonArray + for (anArr in arr) { + list.add(read(anArr)) + } + return list + } + element.isJsonObject -> { + val map: MutableMap = 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 + } +} diff --git a/app/src/main/kotlin/com/simplemobiletools/smsmessenger/helpers/Config.kt b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/helpers/Config.kt index a0dcc48e..842c80be 100644 --- a/app/src/main/kotlin/com/simplemobiletools/smsmessenger/helpers/Config.kt +++ b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/helpers/Config.kt @@ -55,4 +55,24 @@ class Config(context: Context) : BaseConfig(context) { fun removePinnedConversations(conversations: List) { 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() } diff --git a/app/src/main/kotlin/com/simplemobiletools/smsmessenger/helpers/Constants.kt b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/helpers/Constants.kt index c036bfb9..b18c19c8 100644 --- a/app/src/main/kotlin/com/simplemobiletools/smsmessenger/helpers/Constants.kt +++ b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/helpers/Constants.kt @@ -18,6 +18,13 @@ 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" diff --git a/app/src/main/kotlin/com/simplemobiletools/smsmessenger/helpers/MessagesExporter.kt b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/helpers/MessagesExporter.kt new file mode 100644 index 00000000..49c2d4b6 --- /dev/null +++ b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/helpers/MessagesExporter.kt @@ -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) + } + } + } + } +} diff --git a/app/src/main/kotlin/com/simplemobiletools/smsmessenger/helpers/MessagesImporter.kt b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/helpers/MessagesImporter.kt new file mode 100644 index 00000000..4ff1f5b7 --- /dev/null +++ b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/helpers/MessagesImporter.kt @@ -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>() {}.type + val messages = gson.fromJson>(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 + } + ) + } + } +} diff --git a/app/src/main/kotlin/com/simplemobiletools/smsmessenger/helpers/MessagesReader.kt b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/helpers/MessagesReader.kt new file mode 100644 index 00000000..175ad735 --- /dev/null +++ b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/helpers/MessagesReader.kt @@ -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 { + val parts = mutableListOf() + 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 { + val addresses = mutableListOf() + 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 + } + } +} diff --git a/app/src/main/kotlin/com/simplemobiletools/smsmessenger/helpers/MessagesWriter.kt b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/helpers/MessagesWriter.kt new file mode 100644 index 00000000..6065362d --- /dev/null +++ b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/helpers/MessagesWriter.kt @@ -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 + } +} diff --git a/app/src/main/kotlin/com/simplemobiletools/smsmessenger/models/ExportedMessage.kt b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/models/ExportedMessage.kt new file mode 100644 index 00000000..06253d48 --- /dev/null +++ b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/models/ExportedMessage.kt @@ -0,0 +1,10 @@ +package com.simplemobiletools.smsmessenger.models + +import com.google.gson.annotations.SerializedName + +data class ExportedMessage( + @SerializedName("sms") + val sms: List?, + @SerializedName("mms") + val mms: List?, +) diff --git a/app/src/main/kotlin/com/simplemobiletools/smsmessenger/models/MmsAddress.kt b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/models/MmsAddress.kt new file mode 100644 index 00000000..07148905 --- /dev/null +++ b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/models/MmsAddress.kt @@ -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, + ) + } +} diff --git a/app/src/main/kotlin/com/simplemobiletools/smsmessenger/models/MmsBackup.kt b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/models/MmsBackup.kt new file mode 100644 index 00000000..e8957d93 --- /dev/null +++ b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/models/MmsBackup.kt @@ -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, + @SerializedName("parts") + val parts: List, +) { + + 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, + ) + } +} diff --git a/app/src/main/kotlin/com/simplemobiletools/smsmessenger/models/MmsPart.kt b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/models/MmsPart.kt new file mode 100644 index 00000000..ac5d53d1 --- /dev/null +++ b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/models/MmsPart.kt @@ -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") + } +} diff --git a/app/src/main/kotlin/com/simplemobiletools/smsmessenger/models/SmsBackup.kt b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/models/SmsBackup.kt new file mode 100644 index 00000000..a6daa883 --- /dev/null +++ b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/models/SmsBackup.kt @@ -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, + ) + } +} diff --git a/app/src/main/res/layout/dialog_export_messages.xml b/app/src/main/res/layout/dialog_export_messages.xml new file mode 100644 index 00000000..66376d8e --- /dev/null +++ b/app/src/main/res/layout/dialog_export_messages.xml @@ -0,0 +1,70 @@ + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/dialog_import_messages.xml b/app/src/main/res/layout/dialog_import_messages.xml new file mode 100644 index 00000000..0f057a3b --- /dev/null +++ b/app/src/main/res/layout/dialog_import_messages.xml @@ -0,0 +1,35 @@ + + + + + + + + + + + diff --git a/app/src/main/res/menu/menu_main.xml b/app/src/main/res/menu/menu_main.xml index adda1b70..70a319fe 100644 --- a/app/src/main/res/menu/menu_main.xml +++ b/app/src/main/res/menu/menu_main.xml @@ -6,6 +6,14 @@ android:icon="@drawable/ic_search_vector" android:title="@string/search" app:showAsAction="always" /> + + Resize sent MMS images No limit + + Export zpráv + Messages + Export SMS + Export MMS + Check at least one of Export SMS or Export MMS + Import zpráv + Import SMS + Import MMS + Check at least one of Import SMS or Import MMS + Proč aplikace vyžaduje přístup k internetu? 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. diff --git a/app/src/main/res/values-da/strings.xml b/app/src/main/res/values-da/strings.xml index 97decdcf..19068d08 100644 --- a/app/src/main/res/values-da/strings.xml +++ b/app/src/main/res/values-da/strings.xml @@ -55,6 +55,17 @@ Resize sent MMS images No limit + + Messages + Eksporter beskeder + Export SMS + Export MMS + Check at least one of Export SMS or Export MMS + Importer beskeder + Import SMS + Import MMS + Check at least one of Import SMS or Import MMS + Hvorfor kræver appen adgang til internettet? 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. diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 28a67caf..b6d45b31 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -55,6 +55,17 @@ Resize sent MMS images No limit + + Messages + Export messages + Export SMS + Export MMS + Check at least one of Export SMS or Export MMS + Import messages + Import SMS + Import MMS + Check at least one of Import SMS or Import MMS + Warum benötigt diese App Internetzugriff? 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. diff --git a/app/src/main/res/values-el/strings.xml b/app/src/main/res/values-el/strings.xml index fcb3fb4e..8472aaa7 100644 --- a/app/src/main/res/values-el/strings.xml +++ b/app/src/main/res/values-el/strings.xml @@ -55,6 +55,17 @@ Αλλαγή μεγέθους απεσταλμένων εικόνων MMS Χωρίς όριο + + Messages + Εξαγωγή μηνυμάτων + Export SMS + Export MMS + Check at least one of Export SMS or Export MMS + Εισαγωγή μηνυμάτων + Import SMS + Import MMS + Check at least one of Import SMS or Import MMS + Γιατί η εφαρμογή απαιτεί πρόσβαση στο Internet; Δυστυχώς, απαιτείται για την αποστολή συνημμένων MMS. Το να μην είμαστε σε θέση να στείλουμε MMS θα αποτελούσε πραγματικά τεράστιο μειονέκτημα σε σύγκριση με άλλες εφαρμογές, επομένως αποφασίσαμε να ακολουθήσουμε αυτόν τον δρόμο. diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index 7e36ffa0..5f135e98 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -55,6 +55,17 @@ Resize sent MMS images No limit + + Messages + Export messages + Export SMS + Export MMS + Check at least one of Export SMS or Export MMS + Import messages + Import SMS + Import MMS + Check at least one of Import SMS or Import MMS + ¿Por qué la aplicación requiere acceso a internet? 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. diff --git a/app/src/main/res/values-fi/strings.xml b/app/src/main/res/values-fi/strings.xml index 8baaec9f..7d6203f8 100644 --- a/app/src/main/res/values-fi/strings.xml +++ b/app/src/main/res/values-fi/strings.xml @@ -55,6 +55,17 @@ Resize sent MMS images No limit + + Messages + Export messages + Export SMS + Export MMS + Check at least one of Export SMS or Export MMS + Import messages + Import SMS + Import MMS + Check at least one of Import SMS or Import MMS + Miksi sovellus vaatii Internet-yhteyden? 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. diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 0aab6d75..bc36ed18 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -55,6 +55,17 @@ Redimensionner les images MMS envoyées Pas de limite + + Messages + Export de messages + Export SMS + Export MMS + Check at least one of Export SMS or Export MMS + Import de messages + Import SMS + Import MMS + Check at least one of Import SMS or Import MMS + Pourquoi cette application a besoin d\'un accès à internet ? 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. diff --git a/app/src/main/res/values-gl/strings.xml b/app/src/main/res/values-gl/strings.xml index bc222857..a0c42e28 100644 --- a/app/src/main/res/values-gl/strings.xml +++ b/app/src/main/res/values-gl/strings.xml @@ -55,6 +55,17 @@ Resize sent MMS images No limit + + Messages + Export messages + Export SMS + Export MMS + Check at least one of Export SMS or Export MMS + Import messages + Import SMS + Import MMS + Check at least one of Import SMS or Import MMS + Por que o aplicativo necesita acceder a Internet? 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. diff --git a/app/src/main/res/values-id/strings.xml b/app/src/main/res/values-id/strings.xml index 7bbff46f..84e21e67 100644 --- a/app/src/main/res/values-id/strings.xml +++ b/app/src/main/res/values-id/strings.xml @@ -55,6 +55,17 @@ Resize sent MMS images No limit + + Messages + Export messages + Export SMS + Export MMS + Check at least one of Export SMS or Export MMS + Import messages + Import SMS + Import MMS + Check at least one of Import SMS or Import MMS + Mengapa aplikasi membutuhkan akses ke internet? 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. diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index 481992a3..aea63fd7 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -55,6 +55,17 @@ Resize sent MMS images No limit + + Messages + Export messages + Export SMS + Export MMS + Check at least one of Export SMS or Export MMS + Import messages + Import SMS + Import MMS + Check at least one of Import SMS or Import MMS + Perché l\'applicazione richiede l\'accesso ad internet? 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. diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index 681853ed..b890ce8e 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -55,6 +55,17 @@ Resize sent MMS images No limit + + Messages + Export messages + Export SMS + Export MMS + Check at least one of Export SMS or Export MMS + Import messages + Import SMS + Import MMS + Check at least one of Import SMS or Import MMS + なぜアプリ使用にインターネットへのアクセスが必要なのですか? 生憎、MMS(マルチメディアメッセージサービス)にインターネットが必要となります。他のアプリと比較して、MMSを使用出来ないと大きな損になるので、こうすることに決めました。 diff --git a/app/src/main/res/values-lt/strings.xml b/app/src/main/res/values-lt/strings.xml index 0a5b2567..0c5eb136 100644 --- a/app/src/main/res/values-lt/strings.xml +++ b/app/src/main/res/values-lt/strings.xml @@ -15,8 +15,6 @@ Sender doesn\'t support replies Draft Sending… - Export messages - Import messages Pin to the top Unpin @@ -55,6 +53,17 @@ Resize sent MMS images No limit + + Messages + Export messages + Export SMS + Export MMS + Check at least one of Export SMS or Export MMS + Import messages + Import SMS + Import MMS + Check at least one of Import SMS or Import MMS + Why does the app require access to the internet? 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. diff --git a/app/src/main/res/values-ml/strings.xml b/app/src/main/res/values-ml/strings.xml index 0be2d7a7..6bcdf125 100644 --- a/app/src/main/res/values-ml/strings.xml +++ b/app/src/main/res/values-ml/strings.xml @@ -15,8 +15,6 @@ Sender doesn\'t support replies Draft Sending… - Export messages - Import messages Pin to the top Unpin @@ -55,6 +53,17 @@ Resize sent MMS images No limit + + Messages + Export messages + Export SMS + Export MMS + Check at least one of Export SMS or Export MMS + Import messages + Import SMS + Import MMS + Check at least one of Import SMS or Import MMS + അപ്ലിക്കേഷന് ഇന്റർനെറ്റിലേക്ക് ആവശ്യമായി വരുന്നത് എന്തുകൊണ്ട്? നിർഭാഗ്യവശാൽ, MMS അറ്റാച്ചുമെന്റുകൾ അയക്കുന്നതിനു ഇത് ആവശ്യമാണ്. മറ്റ് ആപ്ലിക്കേഷനുകളുമായി താരതമ്യപ്പെടുത്തുമ്പോൾ MMS അയയ്ക്കാൻ കഴിയുന്നില്ല എന്നത് ഒരു വലിയ പോരായ്മയാണ്, അതിനാൽ ഞങ്ങൾ ഈ റൂട്ടിൽ പോകാൻ തീരുമാനിച്ചു. diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml index 6fa59309..aefb2f76 100644 --- a/app/src/main/res/values-nl/strings.xml +++ b/app/src/main/res/values-nl/strings.xml @@ -15,8 +15,6 @@ Afzender ondersteunt geen antwoorden Concept Versturen… - Berichten exporteren - Berichten importeren Pin to the top Unpin @@ -55,6 +53,17 @@ Afbeelding verkleinen voor MMS Geen limiet + + Messages + Berichten exporteren + Export SMS + Export MMS + Check at least one of Export SMS or Export MMS + Berichten importeren + Import SMS + Import MMS + Check at least one of Import SMS or Import MMS + Waarom heeft deze app toegang nodig tot het internet? 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. diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index 7de14ed7..83a69db6 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -15,8 +15,6 @@ Nadawca nie obsługuje odpowiedzi Szkic Wysyłanie… - Eksportuj wiadomości - Importuj wiadomości Przypnij na górze Odepnij @@ -57,6 +55,17 @@ Rozmiar wysyłanych obrazków w MMS-ach Bez limitu + + Messages + Eksportuj wiadomości + Export SMS + Export MMS + Check at least one of Export SMS or Export MMS + Importuj wiadomości + Import SMS + Import MMS + Check at least one of Import SMS or Import MMS + Dlaczego aplikacja wymaga dostępu do Internetu? 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ą. diff --git a/app/src/main/res/values-pt/strings.xml b/app/src/main/res/values-pt/strings.xml index cec92960..26e468e1 100644 --- a/app/src/main/res/values-pt/strings.xml +++ b/app/src/main/res/values-pt/strings.xml @@ -15,8 +15,6 @@ O remetente não aceita respostas Rascunho A enviar… - Exportar mensagens - Importar mensagens Pin to the top Unpin @@ -55,6 +53,17 @@ Resize sent MMS images No limit + + Messages + Exportar mensagens + Export SMS + Export MMS + Check at least one of Export SMS or Export MMS + Importar mensagens + Import SMS + Import MMS + Check at least one of Import SMS or Import MMS + Porque é que a aplicação necessita de aceder à Internet? 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. diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index dab9154e..c2707c1a 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -15,8 +15,6 @@ Отправитель не поддерживает ответы Черновик Отправка… - Экспорт сообщений - Импорт сообщений Pin to the top Unpin @@ -57,6 +55,17 @@ Изменять размер отправляемых в MMS изображений Нет ограничения + + Messages + Экспорт сообщений + Export SMS + Export MMS + Check at least one of Export SMS or Export MMS + Импорт сообщений + Import SMS + Import MMS + Check at least one of Import SMS or Import MMS + Почему приложение требует доступ к интернету? К сожалению, это необходимо для отправки вложений MMS. Отсутствие возможности отправлять MMS-сообщения было бы огромным недостатком нашего приложения по сравнению с другими, поэтому мы решили пойти этим путём. diff --git a/app/src/main/res/values-sk/strings.xml b/app/src/main/res/values-sk/strings.xml index 020a6b05..924233db 100644 --- a/app/src/main/res/values-sk/strings.xml +++ b/app/src/main/res/values-sk/strings.xml @@ -15,8 +15,6 @@ Odosielateľ nepodporuje odpovede Koncept Odosiela sa… - Exportovať správy - Importovať správy Pin to the top Unpin @@ -57,6 +55,17 @@ Zmenšiť MMS obrázky pri odosielaní Žiadny limit + + Správy + Exportovať správy + Exportovať SMS + Exportovať MMS + Označte buď exportovanie SMS alebo exportovanie MMS + Importovať správy + Importovať SMS + Importovať MMS + Označte buď importovanie SMS alebo importovanie MMS + Prečo vyžaduje apka prístup na internet? 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. diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index 2e01517e..90bd5459 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -15,8 +15,6 @@ Sender doesn\'t support replies Draft Sending… - Export messages - Import messages Pin to the top Unpin @@ -55,6 +53,17 @@ Resize sent MMS images No limit + + Messages + Export messages + Export SMS + Export MMS + Check at least one of Export SMS or Export MMS + Import messages + Import SMS + Import MMS + Check at least one of Import SMS or Import MMS + Uygulama neden internete erişim gerektiriyor? 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. diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index a15a8fa3..4ed5486a 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -15,8 +15,6 @@ Sender doesn\'t support replies Draft Sending… - Export messages - Import messages Pin to the top Unpin @@ -55,6 +53,17 @@ Resize sent MMS images No limit + + Messages + Export messages + Export SMS + Export MMS + Check at least one of Export SMS or Export MMS + Import messages + Import SMS + Import MMS + Check at least one of Import SMS or Import MMS + Чому додаток потрубує доступу до інтернету? Нажаль, це необхідно для відправки вкладень MMS. Неспроможність надсилати MMS-повідомлення була б великим недоліком нашого додатку порівняно з іншими, тому ми так зробили. diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index bbce7ff9..9ed50193 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -15,8 +15,6 @@ Sender doesn\'t support replies Draft Sending… - Export messages - Import messages Pin to the top Unpin @@ -55,6 +53,17 @@ Resize sent MMS images No limit + + Messages + Export messages + Export SMS + Export MMS + Check at least one of Export SMS or Export MMS + Import messages + Import SMS + Import MMS + Check at least one of Import SMS or Import MMS + 为什么该应用需要访问互联网? 很遗憾这对于发送彩信附件是必须的。如果不能发送彩信的话这相比其他应用会是一个巨大的劣势,所以我们决定这么采取现在的方式。 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 699ee439..89b51202 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -15,8 +15,6 @@ Sender doesn\'t support replies Draft Sending… - Export messages - Import messages Pin to the top Unpin @@ -55,6 +53,17 @@ Resize sent MMS images No limit + + Messages + Export messages + Export SMS + Export MMS + Import messages + Import SMS + Import MMS + Check at least one of Export SMS or Export MMS + Check at least one of Import SMS or Import MMS + Why does the app require access to the internet? 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.