Merge pull request #685 from esensar/feature/519-xml-import

Add support for importing XML exports
This commit is contained in:
Tibor Kaputa
2023-07-15 18:41:26 +02:00
committed by GitHub
4 changed files with 181 additions and 68 deletions

View File

@@ -582,7 +582,7 @@ class MainActivity : SimpleActivity() {
if (isQPlus()) { if (isQPlus()) {
ExportMessagesDialog(this, config.lastExportPath, true) { file -> ExportMessagesDialog(this, config.lastExportPath, true) { file ->
Intent(Intent.ACTION_CREATE_DOCUMENT).apply { Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
type = EXPORT_MIME_TYPE type = JSON_MIME_TYPE
putExtra(Intent.EXTRA_TITLE, file.name) putExtra(Intent.EXTRA_TITLE, file.name)
addCategory(Intent.CATEGORY_OPENABLE) addCategory(Intent.CATEGORY_OPENABLE)
@@ -626,7 +626,8 @@ class MainActivity : SimpleActivity() {
if (isQPlus()) { if (isQPlus()) {
Intent(Intent.ACTION_GET_CONTENT).apply { Intent(Intent.ACTION_GET_CONTENT).apply {
addCategory(Intent.CATEGORY_OPENABLE) addCategory(Intent.CATEGORY_OPENABLE)
type = EXPORT_MIME_TYPE type = JSON_MIME_TYPE
putExtra(Intent.EXTRA_MIME_TYPES, arrayOf(JSON_MIME_TYPE, XML_MIME_TYPE, TXT_MIME_TYPE))
try { try {
startActivityForResult(this, PICK_IMPORT_SOURCE_INTENT) startActivityForResult(this, PICK_IMPORT_SOURCE_INTENT)
@@ -639,27 +640,27 @@ class MainActivity : SimpleActivity() {
} else { } else {
handlePermission(PERMISSION_READ_STORAGE) { handlePermission(PERMISSION_READ_STORAGE) {
if (it) { if (it) {
importEvents() importMessages()
} }
} }
} }
} }
private fun importEvents() { private fun importMessages() {
FilePickerDialog(this) { FilePickerDialog(this) {
showImportEventsDialog(it) showImportMessagesDialog(it)
} }
} }
private fun showImportEventsDialog(path: String) { private fun showImportMessagesDialog(path: String) {
ImportMessagesDialog(this, path) ImportMessagesDialog(this, path)
} }
private fun tryImportMessagesFromFile(uri: Uri) { private fun tryImportMessagesFromFile(uri: Uri) {
when (uri.scheme) { when (uri.scheme) {
"file" -> showImportEventsDialog(uri.path!!) "file" -> showImportMessagesDialog(uri.path!!)
"content" -> { "content" -> {
val tempFile = getTempFile("messages", "backup.json") var tempFile = getTempFile("messages", "backup.json")
if (tempFile == null) { if (tempFile == null) {
toast(R.string.unknown_error_occurred) toast(R.string.unknown_error_occurred)
return return
@@ -669,7 +670,18 @@ class MainActivity : SimpleActivity() {
val inputStream = contentResolver.openInputStream(uri) val inputStream = contentResolver.openInputStream(uri)
val out = FileOutputStream(tempFile) val out = FileOutputStream(tempFile)
inputStream!!.copyTo(out) inputStream!!.copyTo(out)
showImportEventsDialog(tempFile.absolutePath) // 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) { } catch (e: Exception) {
showErrorToast(e) showErrorToast(e)
} }

View File

@@ -7,7 +7,7 @@ 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.EXPORT_FILE_EXT import com.simplemobiletools.smsmessenger.helpers.JSON_FILE_EXTENSION
import kotlinx.android.synthetic.main.dialog_export_messages.view.* import kotlinx.android.synthetic.main.dialog_export_messages.view.*
import java.io.File import java.io.File
@@ -51,7 +51,7 @@ class ExportMessagesDialog(
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$EXPORT_FILE_EXT") val file = File(realPath, "$filename$JSON_FILE_EXTENSION")
if (!hidePath && file.exists()) { if (!hidePath && file.exists()) {
activity.toast(R.string.name_taken) activity.toast(R.string.name_taken)
return@setOnClickListener return@setOnClickListener

View File

@@ -28,8 +28,10 @@ const val PINNED_CONVERSATIONS = "pinned_conversations"
const val BLOCKED_KEYWORDS = "blocked_keywords" const val BLOCKED_KEYWORDS = "blocked_keywords"
const val EXPORT_SMS = "export_sms" const val EXPORT_SMS = "export_sms"
const val EXPORT_MMS = "export_mms" const val EXPORT_MMS = "export_mms"
const val EXPORT_MIME_TYPE = "application/json" const val JSON_FILE_EXTENSION = ".json"
const val EXPORT_FILE_EXT = ".json" const val JSON_MIME_TYPE = "application/json"
const val XML_MIME_TYPE = "text/xml"
const val TXT_MIME_TYPE = "text/plain"
const val IMPORT_SMS = "import_sms" const val IMPORT_SMS = "import_sms"
const val IMPORT_MMS = "import_mms" const val IMPORT_MMS = "import_mms"
const val WAS_DB_CLEARED = "was_db_cleared_2" const val WAS_DB_CLEARED = "was_db_cleared_2"

View File

@@ -2,6 +2,7 @@ package com.simplemobiletools.smsmessenger.helpers
import android.content.Context import android.content.Context
import android.util.JsonToken import android.util.JsonToken
import android.util.Xml
import com.google.gson.Gson import com.google.gson.Gson
import com.google.gson.reflect.TypeToken import com.google.gson.reflect.TypeToken
import com.simplemobiletools.commons.extensions.showErrorToast import com.simplemobiletools.commons.extensions.showErrorToast
@@ -13,7 +14,9 @@ import com.simplemobiletools.smsmessenger.helpers.MessagesImporter.ImportResult.
import com.simplemobiletools.smsmessenger.helpers.MessagesImporter.ImportResult.IMPORT_PARTIAL import com.simplemobiletools.smsmessenger.helpers.MessagesImporter.ImportResult.IMPORT_PARTIAL
import com.simplemobiletools.smsmessenger.models.MmsBackup import com.simplemobiletools.smsmessenger.models.MmsBackup
import com.simplemobiletools.smsmessenger.models.SmsBackup import com.simplemobiletools.smsmessenger.models.SmsBackup
import org.xmlpull.v1.XmlPullParser
import java.io.File import java.io.File
import java.io.InputStream
class MessagesImporter(private val context: Context) { class MessagesImporter(private val context: Context) {
enum class ImportResult { enum class ImportResult {
@@ -29,63 +32,24 @@ class MessagesImporter(private val context: Context) {
fun importMessages(path: String, onProgress: (total: Int, current: Int) -> Unit = { _, _ -> }, callback: (result: ImportResult) -> Unit) { fun importMessages(path: String, onProgress: (total: Int, current: Int) -> Unit = { _, _ -> }, callback: (result: ImportResult) -> Unit) {
ensureBackgroundThread { ensureBackgroundThread {
try { try {
val inputStream = if (path.contains("/")) { val isXml = if (path.endsWith("txt")) {
File(path).inputStream() // Need to read the first line to determine if it is xml
} else { val tempStream = getInputStreamForPath(path)
context.assets.open(path) tempStream.bufferedReader().use {
} it.readLine().startsWith("<?xml")
inputStream.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()
} }
} else {
jsonReader.endArray() path.endsWith("xml")
} }
val inputStream = getInputStreamForPath(path)
if (isXml) {
inputStream.importXml()
} else {
inputStream.importJson()
}
} catch (e: Exception) { } catch (e: Exception) {
context.showErrorToast(e) context.showErrorToast(e)
messagesFailed++ messagesFailed++
@@ -101,4 +65,139 @@ class MessagesImporter(private val context: Context) {
) )
} }
} }
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() {
bufferedReader().use { 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) {
continue
}
try {
if (xmlParser.name == "sms") {
if (config.importSms) {
val message = xmlParser.readSms()
messageWriter.writeSmsMessage(message)
messagesImported++
} else {
xmlParser.skip()
}
} else {
xmlParser.skip()
}
} catch (e: Exception) {
context.showErrorToast(e)
messagesFailed++
}
}
refreshMessages()
}
}
private fun XmlPullParser.readSms(): SmsBackup {
require(XmlPullParser.START_TAG, null, "sms")
return SmsBackup(
subscriptionId = 0,
address = getAttributeValue(null, "address"),
body = getAttributeValue(null, "body"),
date = getAttributeValue(null, "date").toLong(),
dateSent = getAttributeValue(null, "date").toLong(),
locked = getAttributeValue(null, "locked").toInt(),
protocol = getAttributeValue(null, "protocol"),
read = getAttributeValue(null, "read").toInt(),
status = getAttributeValue(null, "status").toInt(),
type = getAttributeValue(null, "type").toInt(),
serviceCenter = getAttributeValue(null, "service_center")
)
}
private fun XmlPullParser.skip() {
if (eventType != XmlPullParser.START_TAG) {
throw IllegalStateException()
}
var depth = 1
while (depth != 0) {
when (next()) {
XmlPullParser.END_TAG -> depth--
XmlPullParser.START_TAG -> depth++
}
}
}
} }