diff --git a/app/src/main/java/ch/dissem/apps/abit/SettingsFragment.kt b/app/src/main/java/ch/dissem/apps/abit/SettingsFragment.kt index 086d93c..c7dcbbc 100644 --- a/app/src/main/java/ch/dissem/apps/abit/SettingsFragment.kt +++ b/app/src/main/java/ch/dissem/apps/abit/SettingsFragment.kt @@ -20,7 +20,6 @@ import android.app.Activity import android.content.Context import android.content.Intent import android.content.SharedPreferences -import android.net.Uri import android.os.Bundle import android.preference.PreferenceManager import android.support.v4.content.FileProvider.getUriForFile @@ -31,24 +30,14 @@ import ch.dissem.apps.abit.service.Singleton import ch.dissem.apps.abit.synchronization.SyncAdapter import ch.dissem.apps.abit.util.Constants.PREFERENCE_SERVER_POW import ch.dissem.apps.abit.util.Constants.PREFERENCE_TRUSTED_NODE +import ch.dissem.apps.abit.util.Exports import ch.dissem.apps.abit.util.Preferences -import ch.dissem.bitmessage.entity.valueobject.Label -import ch.dissem.bitmessage.exports.ContactExport -import ch.dissem.bitmessage.exports.MessageExport -import ch.dissem.bitmessage.utils.UnixTime -import com.beust.klaxon.JsonArray -import com.beust.klaxon.Parser import com.mikepenz.aboutlibraries.Libs import com.mikepenz.aboutlibraries.LibsBuilder import org.jetbrains.anko.doAsync import org.jetbrains.anko.support.v4.indeterminateProgressDialog import org.jetbrains.anko.support.v4.startActivity import org.jetbrains.anko.uiThread -import java.io.File -import java.io.FileOutputStream -import java.util.zip.ZipEntry -import java.util.zip.ZipInputStream -import java.util.zip.ZipOutputStream /** * @author Christian Basler @@ -101,43 +90,20 @@ class SettingsFragment : PreferenceFragmentCompat(), SharedPreferences.OnSharedP findPreference("export")?.onPreferenceClickListener = Preference.OnPreferenceClickListener { val ctx = context ?: throw IllegalStateException("No context available") - val dialog = indeterminateProgressDialog(R.string.export_data_summary, R.string.export_data) - doAsync { - val exportDirectory = Preferences.getExportDirectory(ctx) - exportDirectory.mkdirs() - val temp = File(exportDirectory, "export-${UnixTime.now}.zip") - ZipOutputStream(FileOutputStream(temp)).use { zip -> - zip.putNextEntry(ZipEntry("contacts.json")) - val addressRepo = Singleton.getAddressRepository(ctx) - val exportContacts = ContactExport.exportContacts(addressRepo.getContacts()) - zip.write( - exportContacts.toJsonString(true).toByteArray() - ) - zip.closeEntry() - - val messageRepo = Singleton.getMessageRepository(ctx) - zip.putNextEntry(ZipEntry("labels.json")) - val exportLabels = MessageExport.exportLabels(messageRepo.getLabels()) - zip.write( - exportLabels.toJsonString(true).toByteArray() - ) - zip.closeEntry() - zip.putNextEntry(ZipEntry("messages.json")) - val exportMessages = MessageExport.exportMessages(messageRepo.getAllMessages()) - zip.write( - exportMessages.toJsonString(true).toByteArray() - ) - zip.closeEntry() - } - - val contentUri = getUriForFile(ctx, "ch.dissem.apps.abit.fileprovider", temp) - val intent = Intent(android.content.Intent.ACTION_SEND) - intent.type = "application/zip" - intent.putExtra(Intent.EXTRA_SUBJECT, "abit-export.zip") - intent.putExtra(Intent.EXTRA_STREAM, contentUri) - startActivityForResult(Intent.createChooser(intent, ""), WRITE_EXPORT_REQUEST_CODE) - uiThread { - dialog.dismiss() + indeterminateProgressDialog(R.string.export_data_summary, R.string.export_data).apply { + doAsync { + val exportDirectory = Preferences.getExportDirectory(ctx) + exportDirectory.mkdirs() + val file = Exports.exportData(exportDirectory, ctx) + val contentUri = getUriForFile(ctx, "ch.dissem.apps.abit.fileprovider", file) + val intent = Intent(android.content.Intent.ACTION_SEND) + intent.type = "application/zip" + intent.putExtra(Intent.EXTRA_SUBJECT, "abit-export.zip") + intent.putExtra(Intent.EXTRA_STREAM, contentUri) + startActivityForResult(Intent.createChooser(intent, ""), WRITE_EXPORT_REQUEST_CODE) + uiThread { + dismiss() + } } } return@OnPreferenceClickListener true @@ -163,53 +129,19 @@ class SettingsFragment : PreferenceFragmentCompat(), SharedPreferences.OnSharedP } } - private fun processEntry(ctx: Context, zipFile: Uri, entry: String, processor: (JsonArray<*>) -> Unit) = - ZipInputStream(ctx.contentResolver.openInputStream(zipFile)).use { zip -> - var nextEntry = zip.nextEntry - while (nextEntry != null) { - if (nextEntry.name == entry) { - processor(Parser().parse(zip) as JsonArray<*>) - } - nextEntry = zip.nextEntry - } - } - override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { val ctx = context ?: throw IllegalStateException("No context available") when (requestCode) { WRITE_EXPORT_REQUEST_CODE -> Preferences.cleanupExportDirectory(ctx) READ_IMPORT_REQUEST_CODE -> { if (resultCode == Activity.RESULT_OK && data?.data != null) { - val dialog = indeterminateProgressDialog(R.string.import_data_summary, R.string.import_data) - doAsync { - val bmc = Singleton.getBitmessageContext(ctx) - val labels = mutableMapOf() - val zipFile = data.data - - processEntry(ctx, zipFile, "contacts.json") { json -> - ContactExport.importContacts(json).forEach { contact -> - bmc.addresses.save(contact) + indeterminateProgressDialog(R.string.import_data_summary, R.string.import_data).apply { + doAsync { + Exports.importData(data.data, ctx) + uiThread { + dismiss() } } - bmc.messages.getLabels().forEach { label -> - labels[label.toString()] = label - } - processEntry(ctx, zipFile, "labels.json") { json -> - MessageExport.importLabels(json).forEach { label -> - if (!labels.contains(label.toString())) { - bmc.messages.save(label) - labels[label.toString()] = label - } - } - } - processEntry(ctx, zipFile, "messages.json") { json -> - MessageExport.importMessages(json, labels).forEach { message -> - bmc.messages.save(message) - } - } - uiThread { - dialog.dismiss() - } } } } @@ -227,25 +159,29 @@ class SettingsFragment : PreferenceFragmentCompat(), SharedPreferences.OnSharedP override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String) { when (key) { - PREFERENCE_TRUSTED_NODE -> { - val node = sharedPreferences.getString(PREFERENCE_TRUSTED_NODE, null) - val ctx = context ?: throw IllegalStateException("No context available") - if (node != null) { - SyncAdapter.startSync(ctx) - } else { - SyncAdapter.stopSync(ctx) - } - } - PREFERENCE_SERVER_POW -> { - val node = sharedPreferences.getString(PREFERENCE_TRUSTED_NODE, null) - if (node != null) { - val ctx = context ?: throw IllegalStateException("No context available") - if (sharedPreferences.getBoolean(PREFERENCE_SERVER_POW, false)) { - SyncAdapter.startPowSync(ctx) - } else { - SyncAdapter.stopPowSync(ctx) - } - } + PREFERENCE_TRUSTED_NODE -> toggleSyncTrustedNode(sharedPreferences) + PREFERENCE_SERVER_POW -> toggleSyncServerPOW(sharedPreferences) + } + } + + private fun toggleSyncTrustedNode(sharedPreferences: SharedPreferences) { + val node = sharedPreferences.getString(PREFERENCE_TRUSTED_NODE, null) + val ctx = context ?: throw IllegalStateException("No context available") + if (node != null) { + SyncAdapter.startSync(ctx) + } else { + SyncAdapter.stopSync(ctx) + } + } + + private fun toggleSyncServerPOW(sharedPreferences: SharedPreferences) { + val node = sharedPreferences.getString(PREFERENCE_TRUSTED_NODE, null) + if (node != null) { + val ctx = context ?: throw IllegalStateException("No context available") + if (sharedPreferences.getBoolean(PREFERENCE_SERVER_POW, false)) { + SyncAdapter.startPowSync(ctx) + } else { + SyncAdapter.stopPowSync(ctx) } } } diff --git a/app/src/main/java/ch/dissem/apps/abit/repository/AndroidAddressRepository.kt b/app/src/main/java/ch/dissem/apps/abit/repository/AndroidAddressRepository.kt index de048ec..fb843d5 100644 --- a/app/src/main/java/ch/dissem/apps/abit/repository/AndroidAddressRepository.kt +++ b/app/src/main/java/ch/dissem/apps/abit/repository/AndroidAddressRepository.kt @@ -17,6 +17,7 @@ package ch.dissem.apps.abit.repository import android.content.ContentValues +import android.database.Cursor import ch.dissem.bitmessage.entity.BitmessageAddress import ch.dissem.bitmessage.entity.payload.V3Pubkey import ch.dissem.bitmessage.entity.payload.V4Pubkey @@ -81,8 +82,8 @@ class AndroidAddressRepository(private val sql: SqlHelper) : AddressRepository { * @return the ordered list of ids (address strings) */ fun getContactIds(): List = findIds( - "private_key IS NULL OR chan = '1'", - "$COLUMN_SUBSCRIBED DESC, $COLUMN_ALIAS IS NULL, $COLUMN_ALIAS, $COLUMN_ADDRESS" + "private_key IS NULL OR chan = '1'", + "$COLUMN_SUBSCRIBED DESC, $COLUMN_ALIAS IS NULL, $COLUMN_ALIAS, $COLUMN_ADDRESS" ) private fun findIds(where: String, orderBy: String): List { @@ -94,9 +95,9 @@ class AndroidAddressRepository(private val sql: SqlHelper) : AddressRepository { val db = sql.readableDatabase db.query( - TABLE_NAME, projection, - where, null, null, null, - orderBy + TABLE_NAME, projection, + where, null, null, null, + orderBy ).use { c -> while (c.moveToNext()) { result.add(c.getString(c.getColumnIndex(COLUMN_ADDRESS))) @@ -114,40 +115,42 @@ class AndroidAddressRepository(private val sql: SqlHelper) : AddressRepository { val db = sql.readableDatabase db.query( - TABLE_NAME, projection, - where, null, null, null, null + TABLE_NAME, projection, + where, null, null, null, null ).use { c -> while (c.moveToNext()) { - val address: BitmessageAddress - - val privateKeyBytes = c.getBlob(c.getColumnIndex(COLUMN_PRIVATE_KEY)) - if (privateKeyBytes != null) { - val privateKey = PrivateKey.read(ByteArrayInputStream(privateKeyBytes)) - address = BitmessageAddress(privateKey) - } else { - address = BitmessageAddress(c.getString(c.getColumnIndex(COLUMN_ADDRESS))) - val publicKeyBytes = c.getBlob(c.getColumnIndex(COLUMN_PUBLIC_KEY)) - if (publicKeyBytes != null) { - var pubkey = Factory.readPubkey(address.version, address - .stream, - ByteArrayInputStream(publicKeyBytes), publicKeyBytes.size, - false) - if (address.version == 4L && pubkey is V3Pubkey) { - pubkey = V4Pubkey(pubkey) - } - address.pubkey = pubkey - } - } - address.alias = c.getString(c.getColumnIndex(COLUMN_ALIAS)) - address.isChan = c.getInt(c.getColumnIndex(COLUMN_CHAN)) == 1 - address.isSubscribed = c.getInt(c.getColumnIndex(COLUMN_SUBSCRIBED)) == 1 - - result.add(address) + result.add(getAddress(c)) } } return result } + private fun getAddress(c: Cursor): BitmessageAddress { + val address: BitmessageAddress + + val privateKeyBytes = c.getBlob(c.getColumnIndex(COLUMN_PRIVATE_KEY)) + if (privateKeyBytes != null) { + address = BitmessageAddress(PrivateKey.read(ByteArrayInputStream(privateKeyBytes))) + } else { + address = BitmessageAddress(c.getString(c.getColumnIndex(COLUMN_ADDRESS))) + c.getBlob(c.getColumnIndex(COLUMN_PUBLIC_KEY))?.let { publicKeyBytes -> + val pubkey = Factory.readPubkey(address.version, address.stream, + ByteArrayInputStream(publicKeyBytes), publicKeyBytes.size, + false) + address.pubkey = if (address.version == 4L && pubkey is V3Pubkey) { + V4Pubkey(pubkey) + } else { + pubkey + } + } + } + address.alias = c.getString(c.getColumnIndex(COLUMN_ALIAS)) + address.isChan = c.getInt(c.getColumnIndex(COLUMN_CHAN)) == 1 + address.isSubscribed = c.getInt(c.getColumnIndex(COLUMN_SUBSCRIBED)) == 1 + + return address + } + override fun save(address: BitmessageAddress) = if (exists(address)) { update(address) } else { @@ -157,8 +160,8 @@ class AndroidAddressRepository(private val sql: SqlHelper) : AddressRepository { private fun exists(address: BitmessageAddress): Boolean { val db = sql.readableDatabase db.rawQuery( - "SELECT COUNT(*) FROM Address WHERE address=?", - arrayOf(address.address) + "SELECT COUNT(*) FROM Address WHERE address=?", + arrayOf(address.address) ).use { cursor -> cursor.moveToFirst() return cursor.getInt(0) > 0 diff --git a/app/src/main/java/ch/dissem/apps/abit/util/Exports.kt b/app/src/main/java/ch/dissem/apps/abit/util/Exports.kt new file mode 100644 index 0000000..88010dc --- /dev/null +++ b/app/src/main/java/ch/dissem/apps/abit/util/Exports.kt @@ -0,0 +1,94 @@ +package ch.dissem.apps.abit.util + +import android.content.Context +import android.net.Uri +import ch.dissem.apps.abit.service.Singleton +import ch.dissem.bitmessage.entity.valueobject.Label +import ch.dissem.bitmessage.exports.ContactExport +import ch.dissem.bitmessage.exports.MessageExport +import ch.dissem.bitmessage.utils.UnixTime +import com.beust.klaxon.JsonArray +import com.beust.klaxon.Parser +import java.io.File +import java.io.FileOutputStream +import java.util.zip.ZipEntry +import java.util.zip.ZipInputStream +import java.util.zip.ZipOutputStream + +/** + * Helper object for data export and import. + */ +object Exports { + + fun exportData(target: File, ctx: Context): File { + val temp = if (target.isDirectory) { + File(target, "export-${UnixTime.now}.zip") + } else { + target + } + ZipOutputStream(FileOutputStream(temp)).use { zip -> + zip.putNextEntry(ZipEntry("contacts.json")) + val addressRepo = Singleton.getAddressRepository(ctx) + val exportContacts = ContactExport.exportContacts(addressRepo.getContacts()) + zip.write( + exportContacts.toJsonString(true).toByteArray() + ) + zip.closeEntry() + + val messageRepo = Singleton.getMessageRepository(ctx) + zip.putNextEntry(ZipEntry("labels.json")) + val exportLabels = MessageExport.exportLabels(messageRepo.getLabels()) + zip.write( + exportLabels.toJsonString(true).toByteArray() + ) + zip.closeEntry() + zip.putNextEntry(ZipEntry("messages.json")) + val exportMessages = MessageExport.exportMessages(messageRepo.getAllMessages()) + zip.write( + exportMessages.toJsonString(true).toByteArray() + ) + zip.closeEntry() + } + + return temp + } + + fun importData(zipFile: Uri, ctx: Context) { + val bmc = Singleton.getBitmessageContext(ctx) + val labels = mutableMapOf() + + processEntry(ctx, zipFile, "contacts.json") { json -> + ContactExport.importContacts(json).forEach { contact -> + bmc.addresses.save(contact) + } + } + bmc.messages.getLabels().forEach { label -> + labels[label.toString()] = label + } + processEntry(ctx, zipFile, "labels.json") { json -> + MessageExport.importLabels(json).forEach { label -> + if (!labels.contains(label.toString())) { + bmc.messages.save(label) + labels[label.toString()] = label + } + } + } + processEntry(ctx, zipFile, "messages.json") { json -> + MessageExport.importMessages(json, labels).forEach { message -> + bmc.messages.save(message) + } + } + } + + private fun processEntry(ctx: Context, zipFile: Uri, entry: String, processor: (JsonArray<*>) -> Unit) = + ZipInputStream(ctx.contentResolver.openInputStream(zipFile)).use { zip -> + var nextEntry = zip.nextEntry + while (nextEntry != null) { + if (nextEntry.name == entry) { + processor(Parser().parse(zip) as JsonArray<*>) + } + nextEntry = zip.nextEntry + } + } + +}