Merge pull request #699 from Merkost/export_import_settings

Export import settings section
This commit is contained in:
Tibor Kaputa
2023-07-24 10:55:26 +02:00
committed by GitHub
19 changed files with 432 additions and 481 deletions

View File

@@ -1,7 +1,10 @@
apply plugin: 'com.android.application' plugins {
apply plugin: 'kotlin-android' id 'com.android.application'
apply plugin: 'kotlin-android-extensions' id 'kotlin-android'
apply plugin: 'kotlin-kapt' id 'kotlin-android-extensions'
id 'kotlin-kapt'
id 'org.jetbrains.kotlin.plugin.serialization' version "$kotlin_version"
}
def keystorePropertiesFile = rootProject.file("keystore.properties") def keystorePropertiesFile = rootProject.file("keystore.properties")
def keystoreProperties = new Properties() def keystoreProperties = new Properties()
@@ -71,6 +74,7 @@ dependencies {
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0' implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
implementation 'com.googlecode.ez-vcard:ez-vcard:0.11.3' implementation 'com.googlecode.ez-vcard:ez-vcard:0.11.3'
implementation 'androidx.lifecycle:lifecycle-process:2.5.1' implementation 'androidx.lifecycle:lifecycle-process:2.5.1'
implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.4.1"
kapt "androidx.room:room-compiler:2.5.2" kapt "androidx.room:room-compiler:2.5.2"
implementation "androidx.room:room-runtime:2.5.2" implementation "androidx.room:room-runtime:2.5.2"

View File

@@ -4,3 +4,27 @@
@org.greenrobot.eventbus.Subscribe <methods>; @org.greenrobot.eventbus.Subscribe <methods>;
} }
-keep enum org.greenrobot.eventbus.ThreadMode { *; } -keep enum org.greenrobot.eventbus.ThreadMode { *; }
# Keep `Companion` object fields of serializable classes.
# This avoids serializer lookup through `getDeclaredClasses` as done for named companion objects.
-if @kotlinx.serialization.Serializable class **
-keepclassmembers class <1> {
static <1>$Companion Companion;
}
# Keep `serializer()` on companion objects (both default and named) of serializable classes.
-if @kotlinx.serialization.Serializable class ** {
static **$* *;
}
-keepclassmembers class <2>$<3> {
kotlinx.serialization.KSerializer serializer(...);
}
# Keep `INSTANCE.serializer()` of serializable objects.
-if @kotlinx.serialization.Serializable class ** {
public static ** INSTANCE;
}
-keepclassmembers class <1> {
public static <1> INSTANCE;
kotlinx.serialization.KSerializer serializer(...);
}

View File

@@ -3,19 +3,15 @@ package com.simplemobiletools.smsmessenger.activities
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.Activity import android.app.Activity
import android.app.role.RoleManager import android.app.role.RoleManager
import android.content.ActivityNotFoundException
import android.content.Intent import android.content.Intent
import android.content.pm.ShortcutInfo import android.content.pm.ShortcutInfo
import android.content.pm.ShortcutManager import android.content.pm.ShortcutManager
import android.graphics.drawable.Icon import android.graphics.drawable.Icon
import android.graphics.drawable.LayerDrawable import android.graphics.drawable.LayerDrawable
import android.net.Uri
import android.os.Bundle import android.os.Bundle
import android.provider.Telephony import android.provider.Telephony
import android.text.TextUtils import android.text.TextUtils
import android.widget.Toast
import androidx.coordinatorlayout.widget.CoordinatorLayout import androidx.coordinatorlayout.widget.CoordinatorLayout
import com.simplemobiletools.commons.dialogs.FilePickerDialog
import com.simplemobiletools.commons.dialogs.PermissionRequiredDialog import com.simplemobiletools.commons.dialogs.PermissionRequiredDialog
import com.simplemobiletools.commons.extensions.* import com.simplemobiletools.commons.extensions.*
import com.simplemobiletools.commons.helpers.* import com.simplemobiletools.commons.helpers.*
@@ -25,8 +21,6 @@ import com.simplemobiletools.smsmessenger.BuildConfig
import com.simplemobiletools.smsmessenger.R import com.simplemobiletools.smsmessenger.R
import com.simplemobiletools.smsmessenger.adapters.ConversationsAdapter import com.simplemobiletools.smsmessenger.adapters.ConversationsAdapter
import com.simplemobiletools.smsmessenger.adapters.SearchResultsAdapter import com.simplemobiletools.smsmessenger.adapters.SearchResultsAdapter
import com.simplemobiletools.smsmessenger.dialogs.ExportMessagesDialog
import com.simplemobiletools.smsmessenger.dialogs.ImportMessagesDialog
import com.simplemobiletools.smsmessenger.extensions.* import com.simplemobiletools.smsmessenger.extensions.*
import com.simplemobiletools.smsmessenger.helpers.* import com.simplemobiletools.smsmessenger.helpers.*
import com.simplemobiletools.smsmessenger.models.Conversation import com.simplemobiletools.smsmessenger.models.Conversation
@@ -37,19 +31,14 @@ import kotlinx.android.synthetic.main.activity_main.*
import org.greenrobot.eventbus.EventBus import org.greenrobot.eventbus.EventBus
import org.greenrobot.eventbus.Subscribe import org.greenrobot.eventbus.Subscribe
import org.greenrobot.eventbus.ThreadMode import org.greenrobot.eventbus.ThreadMode
import java.io.FileOutputStream
import java.io.OutputStream
class MainActivity : SimpleActivity() { class MainActivity : SimpleActivity() {
private val MAKE_DEFAULT_APP_REQUEST = 1 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 storedTextColor = 0
private var storedFontSize = 0 private var storedFontSize = 0
private var lastSearchedText = "" private var lastSearchedText = ""
private var bus: EventBus? = null private var bus: EventBus? = null
private val smsExporter by lazy { MessagesExporter(this) }
private var wasProtectionHandled = false private var wasProtectionHandled = false
@SuppressLint("InlinedApi") @SuppressLint("InlinedApi")
@@ -174,8 +163,6 @@ class MainActivity : SimpleActivity() {
main_menu.getToolbar().setOnMenuItemClickListener { menuItem -> main_menu.getToolbar().setOnMenuItemClickListener { menuItem ->
when (menuItem.itemId) { when (menuItem.itemId) {
R.id.import_messages -> tryImportMessages()
R.id.export_messages -> tryToExportMessages()
R.id.more_apps_from_us -> launchMoreAppsFromUsIntent() R.id.more_apps_from_us -> launchMoreAppsFromUsIntent()
R.id.show_archived -> launchArchivedConversations() R.id.show_archived -> launchArchivedConversations()
R.id.settings -> launchSettings() R.id.settings -> launchSettings()
@@ -200,11 +187,6 @@ class MainActivity : SimpleActivity() {
} else { } else {
finish() 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)
} }
} }
@@ -593,119 +575,6 @@ class MainActivity : SimpleActivity() {
startAboutActivity(R.string.app_name, licenses, BuildConfig.VERSION_NAME, faqItems, true) 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 = JSON_MIME_TYPE
putExtra(Intent.EXTRA_TITLE, file.name)
addCategory(Intent.CATEGORY_OPENABLE)
try {
startActivityForResult(this, PICK_EXPORT_FILE_INTENT)
} catch (e: ActivityNotFoundException) {
toast(R.string.system_service_disabled, Toast.LENGTH_LONG)
} catch (e: Exception) {
showErrorToast(e)
}
}
}
} 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 = JSON_MIME_TYPE
putExtra(Intent.EXTRA_MIME_TYPES, arrayOf(JSON_MIME_TYPE, XML_MIME_TYPE, TXT_MIME_TYPE))
try {
startActivityForResult(this, PICK_IMPORT_SOURCE_INTENT)
} catch (e: ActivityNotFoundException) {
toast(R.string.system_service_disabled, Toast.LENGTH_LONG)
} catch (e: Exception) {
showErrorToast(e)
}
}
} else {
handlePermission(PERMISSION_READ_STORAGE) {
if (it) {
importMessages()
}
}
}
}
private fun importMessages() {
FilePickerDialog(this) {
showImportMessagesDialog(it)
}
}
private fun showImportMessagesDialog(path: String) {
ImportMessagesDialog(this, path)
}
private fun tryImportMessagesFromFile(uri: Uri) {
when (uri.scheme) {
"file" -> showImportMessagesDialog(uri.path!!)
"content" -> {
var 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)
// Check is XML and properly rename
tempFile.bufferedReader().use {
if (it.readLine().startsWith("<?xml")) {
val xmlFile = getTempFile("messages", "backup.xml")
if (xmlFile == null || tempFile?.renameTo(xmlFile) == false) {
toast(R.string.unknown_error_occurred)
return
}
tempFile = xmlFile
}
}
showImportMessagesDialog(tempFile!!.absolutePath)
} catch (e: Exception) {
showErrorToast(e)
}
}
else -> toast(R.string.invalid_file_format)
}
}
@Subscribe(threadMode = ThreadMode.MAIN) @Subscribe(threadMode = ThreadMode.MAIN)
fun refreshMessages(event: Events.RefreshMessages) { fun refreshMessages(event: Events.RefreshMessages) {
initMessenger() initMessenger()

View File

@@ -2,21 +2,30 @@ package com.simplemobiletools.smsmessenger.activities
import android.annotation.TargetApi import android.annotation.TargetApi
import android.content.Intent import android.content.Intent
import android.net.Uri
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
import androidx.activity.result.contract.ActivityResultContracts
import com.simplemobiletools.commons.activities.ManageBlockedNumbersActivity import com.simplemobiletools.commons.activities.ManageBlockedNumbersActivity
import com.simplemobiletools.commons.dialogs.* import com.simplemobiletools.commons.dialogs.*
import com.simplemobiletools.commons.extensions.* import com.simplemobiletools.commons.extensions.*
import com.simplemobiletools.commons.helpers.* import com.simplemobiletools.commons.helpers.*
import com.simplemobiletools.commons.models.RadioItem import com.simplemobiletools.commons.models.RadioItem
import com.simplemobiletools.smsmessenger.R import com.simplemobiletools.smsmessenger.R
import com.simplemobiletools.smsmessenger.dialogs.ExportMessagesDialog
import com.simplemobiletools.smsmessenger.extensions.config import com.simplemobiletools.smsmessenger.extensions.config
import com.simplemobiletools.smsmessenger.helpers.* import com.simplemobiletools.smsmessenger.helpers.*
import com.simplemobiletools.smsmessenger.models.*
import kotlinx.android.synthetic.main.activity_settings.* import kotlinx.android.synthetic.main.activity_settings.*
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import java.util.* import java.util.*
import kotlin.system.exitProcess
class SettingsActivity : SimpleActivity() { class SettingsActivity : SimpleActivity() {
private var blockedNumbersAtPause = -1 private var blockedNumbersAtPause = -1
private val messagesFileType = "application/json"
private val messageImportFileTypes = listOf("application/json", "application/xml", "text/xml")
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
isMaterialActivity = true isMaterialActivity = true
@@ -49,6 +58,8 @@ class SettingsActivity : SimpleActivity() {
setupLockScreenVisibility() setupLockScreenVisibility()
setupMMSFileSizeLimit() setupMMSFileSizeLimit()
setupAppPasswordProtection() setupAppPasswordProtection()
setupMessagesExport()
setupMessagesImport()
updateTextColors(settings_nested_scrollview) updateTextColors(settings_nested_scrollview)
if (blockedNumbersAtPause != -1 && blockedNumbersAtPause != getBlockedNumbers().hashCode()) { if (blockedNumbersAtPause != -1 && blockedNumbersAtPause != getBlockedNumbers().hashCode()) {
@@ -60,12 +71,63 @@ class SettingsActivity : SimpleActivity() {
settings_general_settings_label, settings_general_settings_label,
settings_outgoing_messages_label, settings_outgoing_messages_label,
settings_notifications_label, settings_notifications_label,
settings_security_label settings_security_label,
settings_migrating_label
).forEach { ).forEach {
it.setTextColor(getProperPrimaryColor()) it.setTextColor(getProperPrimaryColor())
} }
} }
private val getContent = registerForActivityResult(ActivityResultContracts.OpenDocument()) { uri ->
if (uri != null) {
MessagesImporter(this).importMessages(uri)
}
}
private val saveDocument = registerForActivityResult(ActivityResultContracts.CreateDocument(messagesFileType)) { uri ->
if (uri != null) {
toast(R.string.exporting)
exportMessages(uri)
}
}
private fun setupMessagesExport() {
settings_export_messages_holder.setOnClickListener {
ExportMessagesDialog(this) { fileName ->
saveDocument.launch(fileName)
}
}
}
private fun setupMessagesImport() {
settings_import_messages_holder.setOnClickListener {
getContent.launch(messageImportFileTypes.toTypedArray())
}
}
private fun exportMessages(uri: Uri) {
ensureBackgroundThread {
try {
MessagesReader(this).getMessagesToExport(config.exportSms, config.exportMms) { messagesToExport ->
if (messagesToExport.isEmpty()) {
toast(R.string.no_entries_for_exporting)
return@getMessagesToExport
}
val json = Json { encodeDefaults = true }
val jsonString = json.encodeToString(messagesToExport)
val outputStream = contentResolver.openOutputStream(uri)!!
outputStream.use {
it.write(jsonString.toByteArray())
}
toast(R.string.exporting_successful)
}
} catch (e: Exception) {
showErrorToast(e)
}
}
}
override fun onPause() { override fun onPause() {
super.onPause() super.onPause()
blockedNumbersAtPause = getBlockedNumbers().hashCode() blockedNumbersAtPause = getBlockedNumbers().hashCode()
@@ -98,7 +160,7 @@ class SettingsActivity : SimpleActivity() {
settings_use_english_holder.setOnClickListener { settings_use_english_holder.setOnClickListener {
settings_use_english.toggle() settings_use_english.toggle()
config.useEnglish = settings_use_english.isChecked config.useEnglish = settings_use_english.isChecked
System.exit(0) exitProcess(0)
} }
} }

View File

@@ -2,42 +2,27 @@ package com.simplemobiletools.smsmessenger.dialogs
import android.view.ViewGroup import android.view.ViewGroup
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import com.simplemobiletools.commons.dialogs.FilePickerDialog
import com.simplemobiletools.commons.extensions.* import com.simplemobiletools.commons.extensions.*
import com.simplemobiletools.smsmessenger.R import com.simplemobiletools.smsmessenger.R
import com.simplemobiletools.smsmessenger.activities.SimpleActivity import com.simplemobiletools.smsmessenger.activities.SimpleActivity
import com.simplemobiletools.smsmessenger.extensions.config import com.simplemobiletools.smsmessenger.extensions.config
import com.simplemobiletools.smsmessenger.helpers.JSON_FILE_EXTENSION import kotlinx.android.synthetic.main.dialog_export_messages.view.export_messages_filename
import kotlinx.android.synthetic.main.dialog_export_messages.view.* import kotlinx.android.synthetic.main.dialog_export_messages.view.export_mms_checkbox
import java.io.File import kotlinx.android.synthetic.main.dialog_export_messages.view.export_sms_checkbox
class ExportMessagesDialog( class ExportMessagesDialog(
private val activity: SimpleActivity, private val activity: SimpleActivity,
private val path: String, private val callback: (fileName: String) -> Unit,
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 private val config = activity.config
init { init {
val view = (activity.layoutInflater.inflate(R.layout.dialog_export_messages, null) as ViewGroup).apply { val view = (activity.layoutInflater.inflate(R.layout.dialog_export_messages, null) as ViewGroup).apply {
export_messages_folder.setText(activity.humanizePath(realPath))
export_messages_filename.setText("${activity.getString(R.string.messages)}_${activity.getCurrentFormattedDateTime()}")
export_sms_checkbox.isChecked = config.exportSms export_sms_checkbox.isChecked = config.exportSms
export_mms_checkbox.isChecked = config.exportMms export_mms_checkbox.isChecked = config.exportMms
export_messages_filename.setText(
if (hidePath) { activity.getString(R.string.messages) + "_" + activity.getCurrentFormattedDateTime()
export_messages_folder_hint.beGone() )
} else {
export_messages_folder.setOnClickListener {
activity.hideKeyboard(export_messages_filename)
FilePickerDialog(activity, realPath, false, showFAB = true) {
export_messages_folder.setText(activity.humanizePath(it))
realPath = it
}
}
}
} }
activity.getAlertDialogBuilder() activity.getAlertDialogBuilder()
@@ -45,29 +30,17 @@ class ExportMessagesDialog(
.setNegativeButton(R.string.cancel, null) .setNegativeButton(R.string.cancel, null)
.apply { .apply {
activity.setupDialogStuff(view, this, R.string.export_messages) { alertDialog -> activity.setupDialogStuff(view, this, R.string.export_messages) { alertDialog ->
alertDialog.showKeyboard(view.export_messages_filename)
alertDialog.getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener { alertDialog.getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener {
config.exportSms = view.export_sms_checkbox.isChecked
config.exportMms = view.export_mms_checkbox.isChecked
val filename = view.export_messages_filename.value val filename = view.export_messages_filename.value
when { when {
filename.isEmpty() -> activity.toast(R.string.empty_name) filename.isEmpty() -> activity.toast(R.string.empty_name)
filename.isAValidFilename() -> { filename.isAValidFilename() -> {
val file = File(realPath, "$filename$JSON_FILE_EXTENSION") callback(filename)
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.no_option_selected)
return@setOnClickListener
}
config.exportSms = view.export_sms_checkbox.isChecked
config.exportMms = view.export_mms_checkbox.isChecked
config.lastExportPath = file.absolutePath.getParentPath()
callback(file)
alertDialog.dismiss() alertDialog.dismiss()
} }
else -> activity.toast(R.string.invalid_name) else -> activity.toast(R.string.invalid_name)
} }
} }

View File

@@ -10,13 +10,13 @@ import com.simplemobiletools.smsmessenger.R
import com.simplemobiletools.smsmessenger.activities.SimpleActivity import com.simplemobiletools.smsmessenger.activities.SimpleActivity
import com.simplemobiletools.smsmessenger.extensions.config import com.simplemobiletools.smsmessenger.extensions.config
import com.simplemobiletools.smsmessenger.helpers.MessagesImporter import com.simplemobiletools.smsmessenger.helpers.MessagesImporter
import com.simplemobiletools.smsmessenger.helpers.MessagesImporter.ImportResult.IMPORT_OK import com.simplemobiletools.smsmessenger.models.MessagesBackup
import com.simplemobiletools.smsmessenger.helpers.MessagesImporter.ImportResult.IMPORT_PARTIAL import com.simplemobiletools.smsmessenger.models.ImportResult
import kotlinx.android.synthetic.main.dialog_import_messages.view.* import kotlinx.android.synthetic.main.dialog_import_messages.view.*
class ImportMessagesDialog( class ImportMessagesDialog(
private val activity: SimpleActivity, private val activity: SimpleActivity,
private val path: String, private val messages: List<MessagesBackup>,
) { ) {
private val config = activity.config private val config = activity.config
@@ -48,7 +48,7 @@ class ImportMessagesDialog(
config.importSms = view.import_sms_checkbox.isChecked config.importSms = view.import_sms_checkbox.isChecked
config.importMms = view.import_mms_checkbox.isChecked config.importMms = view.import_mms_checkbox.isChecked
ensureBackgroundThread { ensureBackgroundThread {
MessagesImporter(activity).importMessages(path) { MessagesImporter(activity).restoreMessages(messages) {
handleParseResult(it) handleParseResult(it)
alertDialog.dismiss() alertDialog.dismiss()
} }
@@ -58,11 +58,12 @@ class ImportMessagesDialog(
} }
} }
private fun handleParseResult(result: MessagesImporter.ImportResult) { private fun handleParseResult(result: ImportResult) {
activity.toast( activity.toast(
when (result) { when (result) {
IMPORT_OK -> R.string.importing_successful ImportResult.IMPORT_OK -> R.string.importing_successful
IMPORT_PARTIAL -> R.string.importing_some_entries_failed ImportResult.IMPORT_PARTIAL -> R.string.importing_some_entries_failed
ImportResult.IMPORT_FAIL -> R.string.importing_failed
else -> R.string.no_items_found else -> R.string.no_items_found
} }
) )

View File

@@ -1,68 +0,0 @@
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

@@ -1,172 +1,145 @@
package com.simplemobiletools.smsmessenger.helpers package com.simplemobiletools.smsmessenger.helpers
import android.content.Context import android.net.Uri
import android.util.JsonToken
import android.util.Xml import android.util.Xml
import com.google.gson.Gson
import com.google.gson.reflect.TypeToken
import com.simplemobiletools.commons.extensions.showErrorToast import com.simplemobiletools.commons.extensions.showErrorToast
import com.simplemobiletools.commons.extensions.toast
import com.simplemobiletools.commons.helpers.ensureBackgroundThread import com.simplemobiletools.commons.helpers.ensureBackgroundThread
import com.simplemobiletools.smsmessenger.R
import com.simplemobiletools.smsmessenger.activities.SimpleActivity
import com.simplemobiletools.smsmessenger.dialogs.ImportMessagesDialog
import com.simplemobiletools.smsmessenger.extensions.config import com.simplemobiletools.smsmessenger.extensions.config
import com.simplemobiletools.smsmessenger.helpers.MessagesImporter.ImportResult.IMPORT_FAIL import com.simplemobiletools.smsmessenger.models.*
import com.simplemobiletools.smsmessenger.helpers.MessagesImporter.ImportResult.IMPORT_NOTHING_NEW import kotlinx.serialization.SerializationException
import com.simplemobiletools.smsmessenger.helpers.MessagesImporter.ImportResult.IMPORT_OK import kotlinx.serialization.decodeFromString
import com.simplemobiletools.smsmessenger.helpers.MessagesImporter.ImportResult.IMPORT_PARTIAL import kotlinx.serialization.json.Json
import com.simplemobiletools.smsmessenger.models.MmsBackup
import com.simplemobiletools.smsmessenger.models.SmsBackup
import org.xmlpull.v1.XmlPullParser import org.xmlpull.v1.XmlPullParser
import java.io.File
import java.io.InputStream import java.io.InputStream
class MessagesImporter(private val context: Context) {
enum class ImportResult {
IMPORT_FAIL, IMPORT_OK, IMPORT_PARTIAL, IMPORT_NOTHING_NEW
}
private val gson = Gson() class MessagesImporter(private val activity: SimpleActivity) {
private val messageWriter = MessagesWriter(context)
private val config = context.config private val messageWriter = MessagesWriter(activity)
private val config = activity.config
private var messagesImported = 0 private var messagesImported = 0
private var messagesFailed = 0 private var messagesFailed = 0
fun importMessages(path: String, onProgress: (total: Int, current: Int) -> Unit = { _, _ -> }, callback: (result: ImportResult) -> Unit) { fun importMessages(uri: Uri) {
try {
val fileType = activity.contentResolver.getType(uri).orEmpty()
val isXml = isXmlMimeType(fileType) || (uri.path?.endsWith("txt") == true && isFileXml(uri))
if (isXml) {
activity.toast(R.string.importing)
getInputStreamFromUri(uri)!!.importXml()
} else {
importJson(uri)
}
} catch (e: Exception) {
activity.showErrorToast(e)
}
}
private fun importJson(uri: Uri) {
try {
val jsonString = activity.contentResolver.openInputStream(uri)!!.use { inputStream ->
inputStream.bufferedReader().readText()
}
val deserializedList = Json.decodeFromString<List<MessagesBackup>>(jsonString)
if (deserializedList.isEmpty()) {
activity.toast(R.string.no_entries_for_importing)
return
}
ImportMessagesDialog(activity, deserializedList)
} catch (e: SerializationException) {
activity.toast(R.string.invalid_file_format)
} catch (e: IllegalArgumentException) {
activity.toast(R.string.invalid_file_format)
} catch (e: Exception) {
activity.showErrorToast(e)
}
}
fun restoreMessages(messagesBackup: List<MessagesBackup>, callback: (ImportResult) -> Unit) {
ensureBackgroundThread { ensureBackgroundThread {
try { try {
val isXml = if (path.endsWith("txt")) { messagesBackup.forEach { message ->
// Need to read the first line to determine if it is xml try {
val tempStream = getInputStreamForPath(path) if (message.backupType == BackupType.SMS && config.importSms) {
tempStream.bufferedReader().use { messageWriter.writeSmsMessage(message as SmsBackup)
it.readLine().startsWith("<?xml") messagesImported++
} else if (message.backupType == BackupType.MMS && config.importMms) {
messageWriter.writeMmsMessage(message as MmsBackup)
messagesImported++
}
} catch (e: Exception) {
activity.showErrorToast(e)
messagesFailed++
} }
} else {
path.endsWith("xml")
} }
refreshMessages()
val inputStream = getInputStreamForPath(path)
if (isXml) {
inputStream.importXml()
} else {
inputStream.importJson()
}
} catch (e: Exception) { } catch (e: Exception) {
context.showErrorToast(e) activity.showErrorToast(e)
messagesFailed++
} }
callback.invoke( callback.invoke(
when { when {
messagesImported == 0 && messagesFailed == 0 -> IMPORT_NOTHING_NEW messagesImported == 0 && messagesFailed == 0 -> ImportResult.IMPORT_NOTHING_NEW
messagesFailed > 0 && messagesImported > 0 -> IMPORT_PARTIAL messagesFailed > 0 && messagesImported > 0 -> ImportResult.IMPORT_PARTIAL
messagesFailed > 0 -> IMPORT_FAIL messagesFailed > 0 -> ImportResult.IMPORT_FAIL
else -> IMPORT_OK else -> ImportResult.IMPORT_OK
} }
) )
} }
} }
private fun getInputStreamForPath(path: String): InputStream {
return if (path.contains("/")) {
File(path).inputStream()
} else {
context.assets.open(path)
}
}
private fun InputStream.importJson() {
bufferedReader().use { reader ->
val jsonReader = gson.newJsonReader(reader)
val smsMessageType = object : TypeToken<SmsBackup>() {}.type
val mmsMessageType = object : TypeToken<MmsBackup>() {}.type
jsonReader.beginArray()
while (jsonReader.hasNext()) {
jsonReader.beginObject()
while (jsonReader.hasNext()) {
val nextToken = jsonReader.peek()
if (nextToken.ordinal == JsonToken.NAME.ordinal) {
val msgType = jsonReader.nextName()
if ((!msgType.equals("sms") && !msgType.equals("mms")) ||
(msgType.equals("sms") && !config.importSms) ||
(msgType.equals("mms") && !config.importMms)
) {
jsonReader.skipValue()
continue
}
jsonReader.beginArray()
while (jsonReader.hasNext()) {
try {
if (msgType.equals("sms")) {
val message = gson.fromJson<SmsBackup>(jsonReader, smsMessageType)
messageWriter.writeSmsMessage(message)
} else {
val message = gson.fromJson<MmsBackup>(jsonReader, mmsMessageType)
messageWriter.writeMmsMessage(message)
}
messagesImported++
} catch (e: Exception) {
context.showErrorToast(e)
messagesFailed++
}
}
jsonReader.endArray()
} else {
jsonReader.skipValue()
}
}
jsonReader.endObject()
refreshMessages()
}
jsonReader.endArray()
}
}
private fun InputStream.importXml() { private fun InputStream.importXml() {
bufferedReader().use { reader -> try {
val xmlParser = Xml.newPullParser().apply { bufferedReader().use { reader ->
setInput(reader) val xmlParser = Xml.newPullParser().apply {
} setInput(reader)
xmlParser.nextTag()
xmlParser.require(XmlPullParser.START_TAG, null, "smses")
var depth = 1
while (depth != 0) {
when (xmlParser.next()) {
XmlPullParser.END_TAG -> depth--
XmlPullParser.START_TAG -> depth++
} }
if (xmlParser.eventType != XmlPullParser.START_TAG) { xmlParser.nextTag()
continue xmlParser.require(XmlPullParser.START_TAG, null, "smses")
}
try { var depth = 1
if (xmlParser.name == "sms") { while (depth != 0) {
if (config.importSms) { when (xmlParser.next()) {
val message = xmlParser.readSms() XmlPullParser.END_TAG -> depth--
messageWriter.writeSmsMessage(message) XmlPullParser.START_TAG -> depth++
messagesImported++ }
if (xmlParser.eventType != XmlPullParser.START_TAG) {
continue
}
try {
if (xmlParser.name == "sms") {
if (config.importSms) {
val message = xmlParser.readSms()
messageWriter.writeSmsMessage(message)
messagesImported++
} else {
xmlParser.skip()
}
} else { } else {
xmlParser.skip() xmlParser.skip()
} }
} else { } catch (e: Exception) {
xmlParser.skip() activity.showErrorToast(e)
messagesFailed++
} }
} catch (e: Exception) {
context.showErrorToast(e)
messagesFailed++
} }
refreshMessages()
} }
when {
refreshMessages() messagesFailed > 0 && messagesImported > 0 -> activity.toast(R.string.importing_some_entries_failed)
messagesFailed > 0 -> activity.toast(R.string.importing_failed)
else -> activity.toast(R.string.importing_successful)
}
} catch (_: Exception) {
activity.toast(R.string.invalid_file_format)
} }
} }
@@ -200,4 +173,27 @@ class MessagesImporter(private val context: Context) {
} }
} }
} }
private fun getInputStreamFromUri(uri: Uri): InputStream? {
return try {
activity.contentResolver.openInputStream(uri)
} catch (e: Exception) {
null
}
}
private fun isFileXml(uri: Uri): Boolean {
val inputStream = getInputStreamFromUri(uri)
return inputStream?.bufferedReader()?.use { reader ->
reader.readLine()?.startsWith("<?xml") ?: false
} ?: false
}
private fun isXmlMimeType(mimeType: String): Boolean {
return mimeType.equals("application/xml", ignoreCase = true) || mimeType.equals("text/xml", ignoreCase = true)
}
private fun isJsonMimeType(mimeType: String): Boolean {
return mimeType.equals("application/json", ignoreCase = true)
}
} }

View File

@@ -9,15 +9,30 @@ import android.util.Base64
import com.simplemobiletools.commons.extensions.* import com.simplemobiletools.commons.extensions.*
import com.simplemobiletools.commons.helpers.isQPlus import com.simplemobiletools.commons.helpers.isQPlus
import com.simplemobiletools.commons.helpers.isRPlus import com.simplemobiletools.commons.helpers.isRPlus
import com.simplemobiletools.smsmessenger.models.MmsAddress import com.simplemobiletools.smsmessenger.extensions.getConversationIds
import com.simplemobiletools.smsmessenger.models.MmsBackup import com.simplemobiletools.smsmessenger.models.*
import com.simplemobiletools.smsmessenger.models.MmsPart
import com.simplemobiletools.smsmessenger.models.SmsBackup
import java.io.IOException import java.io.IOException
import java.io.InputStream import java.io.InputStream
class MessagesReader(private val context: Context) { class MessagesReader(private val context: Context) {
fun forEachSms(threadId: Long, block: (SmsBackup) -> Unit) {
fun getMessagesToExport(
getSms: Boolean, getMms: Boolean, callback: (messages: List<MessagesBackup>) -> Unit
) {
val conversationIds = context.getConversationIds()
var smsMessages = listOf<SmsBackup>()
var mmsMessages = listOf<MmsBackup>()
if (getSms) {
smsMessages = getSmsMessages(conversationIds)
}
if (getMms) {
mmsMessages = getMmsMessages(conversationIds)
}
callback(smsMessages + mmsMessages)
}
private fun getSmsMessages(threadIds: List<Long>): List<SmsBackup> {
val projection = arrayOf( val projection = arrayOf(
Sms.SUBSCRIPTION_ID, Sms.SUBSCRIPTION_ID,
Sms.ADDRESS, Sms.ADDRESS,
@@ -33,25 +48,28 @@ class MessagesReader(private val context: Context) {
) )
val selection = "${Sms.THREAD_ID} = ?" val selection = "${Sms.THREAD_ID} = ?"
val selectionArgs = arrayOf(threadId.toString()) val smsList = mutableListOf<SmsBackup>()
context.queryCursor(Sms.CONTENT_URI, projection, selection, selectionArgs) { cursor ->
val subscriptionId = cursor.getLongValue(Sms.SUBSCRIPTION_ID) threadIds.map { it.toString() }.forEach { threadId ->
val address = cursor.getStringValue(Sms.ADDRESS) context.queryCursor(Sms.CONTENT_URI, projection, selection, arrayOf(threadId)) { cursor ->
val body = cursor.getStringValueOrNull(Sms.BODY) val subscriptionId = cursor.getLongValue(Sms.SUBSCRIPTION_ID)
val date = cursor.getLongValue(Sms.DATE) val address = cursor.getStringValue(Sms.ADDRESS)
val dateSent = cursor.getLongValue(Sms.DATE_SENT) val body = cursor.getStringValueOrNull(Sms.BODY)
val locked = cursor.getIntValue(Sms.LOCKED) val date = cursor.getLongValue(Sms.DATE)
val protocol = cursor.getStringValueOrNull(Sms.PROTOCOL) val dateSent = cursor.getLongValue(Sms.DATE_SENT)
val read = cursor.getIntValue(Sms.READ) val locked = cursor.getIntValue(Sms.DATE_SENT)
val status = cursor.getIntValue(Sms.STATUS) val protocol = cursor.getStringValueOrNull(Sms.PROTOCOL)
val type = cursor.getIntValue(Sms.TYPE) val read = cursor.getIntValue(Sms.READ)
val serviceCenter = cursor.getStringValueOrNull(Sms.SERVICE_CENTER) val status = cursor.getIntValue(Sms.STATUS)
block(SmsBackup(subscriptionId, address, body, date, dateSent, locked, protocol, read, status, type, serviceCenter)) val type = cursor.getIntValue(Sms.TYPE)
val serviceCenter = cursor.getStringValueOrNull(Sms.SERVICE_CENTER)
smsList.add(SmsBackup(subscriptionId, address, body, date, dateSent, locked, protocol, read, status, type, serviceCenter))
}
} }
return smsList
} }
// all mms from simple sms are non-text messages private fun getMmsMessages(threadIds: List<Long>, includeTextOnlyAttachment: Boolean = false): List<MmsBackup> {
fun forEachMms(threadId: Long, includeTextOnlyAttachment: Boolean = false, block: (MmsBackup) -> Unit) {
val projection = arrayOf( val projection = arrayOf(
Mms._ID, Mms._ID,
Mms.CREATOR, Mms.CREATOR,
@@ -71,65 +89,67 @@ class MessagesReader(private val context: Context) {
Mms.SUBSCRIPTION_ID, Mms.SUBSCRIPTION_ID,
Mms.TRANSACTION_ID Mms.TRANSACTION_ID
) )
val selection = if (includeTextOnlyAttachment) { val selection = if (includeTextOnlyAttachment) {
"${Mms.THREAD_ID} = ? AND ${Mms.TEXT_ONLY} = ?" "${Mms.THREAD_ID} = ? AND ${Mms.TEXT_ONLY} = ?"
} else { } else {
"${Mms.THREAD_ID} = ?" "${Mms.THREAD_ID} = ?"
} }
val mmsList = mutableListOf<MmsBackup>()
val selectionArgs = if (includeTextOnlyAttachment) { threadIds.map { it.toString() }.forEach { threadId ->
arrayOf(threadId.toString(), "1") val selectionArgs = if (includeTextOnlyAttachment) {
} else { arrayOf(threadId, "1")
arrayOf(threadId.toString()) } else {
} arrayOf(threadId)
}
context.queryCursor(Mms.CONTENT_URI, projection, selection, selectionArgs) { cursor ->
val mmsId = cursor.getLongValue(Mms._ID)
val creator = cursor.getStringValueOrNull(Mms.CREATOR)
val contentType = cursor.getStringValueOrNull(Mms.CONTENT_TYPE)
val deliveryReport = cursor.getIntValue(Mms.DELIVERY_REPORT)
val date = cursor.getLongValue(Mms.DATE)
val dateSent = cursor.getLongValue(Mms.DATE_SENT)
val locked = cursor.getIntValue(Mms.LOCKED)
val messageType = cursor.getIntValue(Mms.MESSAGE_TYPE)
val messageBox = cursor.getIntValue(Mms.MESSAGE_BOX)
val read = cursor.getIntValue(Mms.READ)
val readReport = cursor.getIntValue(Mms.READ_REPORT)
val seen = cursor.getIntValue(Mms.SEEN)
val textOnly = cursor.getIntValue(Mms.TEXT_ONLY)
val status = cursor.getStringValueOrNull(Mms.STATUS)
val subject = cursor.getStringValueOrNull(Mms.SUBJECT)
val subjectCharSet = cursor.getStringValueOrNull(Mms.SUBJECT_CHARSET)
val subscriptionId = cursor.getLongValue(Mms.SUBSCRIPTION_ID)
val transactionId = cursor.getStringValueOrNull(Mms.TRANSACTION_ID)
context.queryCursor(Mms.CONTENT_URI, projection, selection, selectionArgs) { cursor -> val parts = getParts(mmsId)
val mmsId = cursor.getLongValue(Mms._ID) val addresses = getMmsAddresses(mmsId)
val creator = cursor.getStringValueOrNull(Mms.CREATOR) mmsList.add(
val contentType = cursor.getStringValueOrNull(Mms.CONTENT_TYPE) MmsBackup(
val deliveryReport = cursor.getIntValue(Mms.DELIVERY_REPORT) creator,
val date = cursor.getLongValue(Mms.DATE) contentType,
val dateSent = cursor.getLongValue(Mms.DATE_SENT) deliveryReport,
val locked = cursor.getIntValue(Mms.LOCKED) date,
val messageType = cursor.getIntValue(Mms.MESSAGE_TYPE) dateSent,
val messageBox = cursor.getIntValue(Mms.MESSAGE_BOX) locked,
val read = cursor.getIntValue(Mms.READ) messageType,
val readReport = cursor.getIntValue(Mms.READ_REPORT) messageBox,
val seen = cursor.getIntValue(Mms.SEEN) read,
val textOnly = cursor.getIntValue(Mms.TEXT_ONLY) readReport,
val status = cursor.getStringValueOrNull(Mms.STATUS) seen,
val subject = cursor.getStringValueOrNull(Mms.SUBJECT) textOnly,
val subjectCharSet = cursor.getStringValueOrNull(Mms.SUBJECT_CHARSET) status,
val subscriptionId = cursor.getLongValue(Mms.SUBSCRIPTION_ID) subject,
val transactionId = cursor.getStringValueOrNull(Mms.TRANSACTION_ID) subjectCharSet,
subscriptionId,
val parts = getParts(mmsId) transactionId,
val addresses = getMmsAddresses(mmsId) addresses,
block( parts
MmsBackup( )
creator,
contentType,
deliveryReport,
date,
dateSent,
locked,
messageType,
messageBox,
read,
readReport,
seen,
textOnly,
status,
subject,
subjectCharSet,
subscriptionId,
transactionId,
addresses,
parts
) )
) }
} }
return mmsList
} }
@SuppressLint("NewApi") @SuppressLint("NewApi")

View File

@@ -0,0 +1,13 @@
package com.simplemobiletools.smsmessenger.models
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
enum class BackupType {
@SerialName("sms")
SMS,
@SerialName("mms")
MMS,
}

View File

@@ -0,0 +1,5 @@
package com.simplemobiletools.smsmessenger.models
enum class ImportResult {
IMPORT_FAIL, IMPORT_OK, IMPORT_PARTIAL, IMPORT_NOTHING_NEW
}

View File

@@ -0,0 +1,24 @@
package com.simplemobiletools.smsmessenger.models
import kotlinx.serialization.DeserializationStrategy
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.SerializationException
import kotlinx.serialization.json.*
@Serializable(with = BackupSerializer::class)
sealed class MessagesBackup() {
@SerialName("backupType")
abstract val backupType: BackupType
}
object BackupSerializer :
JsonContentPolymorphicSerializer<MessagesBackup>(MessagesBackup::class) {
override fun selectDeserializer(element: JsonElement): DeserializationStrategy<out MessagesBackup> {
return when (element.jsonObject["backupType"]?.jsonPrimitive?.content) {
"sms" -> SmsBackup.serializer()
"mms" -> MmsBackup.serializer()
else -> throw SerializationException("ERROR: No Serializer found. Serialization failed.")
}
}
}

View File

@@ -4,7 +4,9 @@ import android.content.ContentValues
import android.provider.Telephony import android.provider.Telephony
import androidx.core.content.contentValuesOf import androidx.core.content.contentValuesOf
import com.google.gson.annotations.SerializedName import com.google.gson.annotations.SerializedName
import kotlinx.serialization.Serializable
@Serializable
data class MmsAddress( data class MmsAddress(
@SerializedName("address") @SerializedName("address")
val address: String, val address: String,

View File

@@ -4,7 +4,9 @@ import android.content.ContentValues
import android.provider.Telephony import android.provider.Telephony
import androidx.core.content.contentValuesOf import androidx.core.content.contentValuesOf
import com.google.gson.annotations.SerializedName import com.google.gson.annotations.SerializedName
import kotlinx.serialization.Serializable
@Serializable
data class MmsBackup( data class MmsBackup(
@SerializedName("creator") @SerializedName("creator")
val creator: String?, val creator: String?,
@@ -44,7 +46,9 @@ data class MmsBackup(
val addresses: List<MmsAddress>, val addresses: List<MmsAddress>,
@SerializedName("parts") @SerializedName("parts")
val parts: List<MmsPart>, val parts: List<MmsPart>,
) {
override val backupType: BackupType = BackupType.MMS,
): MessagesBackup() {
fun toContentValues(): ContentValues { fun toContentValues(): ContentValues {
return contentValuesOf( return contentValuesOf(

View File

@@ -4,7 +4,9 @@ import android.content.ContentValues
import android.provider.Telephony import android.provider.Telephony
import androidx.core.content.contentValuesOf import androidx.core.content.contentValuesOf
import com.google.gson.annotations.SerializedName import com.google.gson.annotations.SerializedName
import kotlinx.serialization.Serializable
@Serializable
data class MmsPart( data class MmsPart(
@SerializedName("cd") @SerializedName("cd")
val contentDisposition: String?, val contentDisposition: String?,

View File

@@ -5,7 +5,9 @@ import android.content.ContentValues
import android.provider.Telephony import android.provider.Telephony
import androidx.core.content.contentValuesOf import androidx.core.content.contentValuesOf
import com.google.gson.annotations.SerializedName import com.google.gson.annotations.SerializedName
import kotlinx.serialization.Serializable
@Serializable
data class SmsBackup( data class SmsBackup(
@SerializedName("sub_id") @SerializedName("sub_id")
val subscriptionId: Long, val subscriptionId: Long,
@@ -28,8 +30,10 @@ data class SmsBackup(
@SerializedName("type") @SerializedName("type")
val type: Int, val type: Int,
@SerializedName("service_center") @SerializedName("service_center")
val serviceCenter: String? val serviceCenter: String?,
) {
override val backupType: BackupType = BackupType.SMS,
): MessagesBackup() {
fun toContentValues(): ContentValues { fun toContentValues(): ContentValues {
return contentValuesOf( return contentValuesOf(

View File

@@ -382,6 +382,47 @@
android:text="@string/password_protect_whole_app" /> android:text="@string/password_protect_whole_app" />
</RelativeLayout> </RelativeLayout>
<include
android:id="@+id/settings_migrating_divider"
layout="@layout/divider" />
<TextView
android:id="@+id/settings_migrating_label"
style="@style/SettingsSectionLabelStyle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/migrating" />
<RelativeLayout
android:id="@+id/settings_export_messages_holder"
style="@style/SettingsHolderTextViewOneLinerStyle"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<com.simplemobiletools.commons.views.MyTextView
android:id="@+id/settings_export_messages"
style="@style/SettingsTextLabelStyle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/export_messages" />
</RelativeLayout>
<RelativeLayout
android:id="@+id/settings_import_messages_holder"
style="@style/SettingsHolderTextViewOneLinerStyle"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<com.simplemobiletools.commons.views.MyTextView
android:id="@+id/settings_import_messages"
style="@style/SettingsTextLabelStyle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/import_messages" />
</RelativeLayout>
</LinearLayout> </LinearLayout>
</androidx.core.widget.NestedScrollView> </androidx.core.widget.NestedScrollView>
</androidx.coordinatorlayout.widget.CoordinatorLayout> </androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@@ -14,21 +14,6 @@
android:paddingTop="@dimen/activity_margin" android:paddingTop="@dimen/activity_margin"
android:paddingEnd="@dimen/activity_margin"> android:paddingEnd="@dimen/activity_margin">
<com.simplemobiletools.commons.views.MyTextInputLayout
android:id="@+id/export_messages_folder_hint"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="@dimen/activity_margin"
android:hint="@string/folder">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/export_messages_folder"
style="@style/UnclickableEditText"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</com.simplemobiletools.commons.views.MyTextInputLayout>
<com.simplemobiletools.commons.views.MyTextInputLayout <com.simplemobiletools.commons.views.MyTextInputLayout
android:id="@+id/export_messages_filename_hint" android:id="@+id/export_messages_filename_hint"
android:layout_width="match_parent" android:layout_width="match_parent"

View File

@@ -3,16 +3,6 @@
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
tools:ignore="AppCompatResource,AlwaysShowAction"> tools:ignore="AppCompatResource,AlwaysShowAction">
<item
android:id="@+id/import_messages"
android:showAsAction="never"
android:title="@string/import_messages"
app:showAsAction="never" />
<item
android:id="@+id/export_messages"
android:showAsAction="never"
android:title="@string/export_messages"
app:showAsAction="never" />
<item <item
android:id="@+id/show_archived" android:id="@+id/show_archived"
android:showAsAction="never" android:showAsAction="never"