Imports basically work now, although there may be some issues if the user switches language between export and import.

This commit is contained in:
Christian Basler 2017-08-17 17:29:29 +02:00
parent 625848bd9d
commit 858651e808
5 changed files with 481 additions and 432 deletions

View File

@ -16,9 +16,11 @@
package ch.dissem.apps.abit package ch.dissem.apps.abit
import android.app.Activity
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.SharedPreferences import android.content.SharedPreferences
import android.net.Uri
import android.os.Bundle import android.os.Bundle
import android.preference.PreferenceManager import android.preference.PreferenceManager
import android.support.v4.content.FileProvider.getUriForFile import android.support.v4.content.FileProvider.getUriForFile
@ -30,21 +32,23 @@ 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_SERVER_POW
import ch.dissem.apps.abit.util.Constants.PREFERENCE_TRUSTED_NODE import ch.dissem.apps.abit.util.Constants.PREFERENCE_TRUSTED_NODE
import ch.dissem.apps.abit.util.Preferences 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.ContactExport
import ch.dissem.bitmessage.exports.MessageExport import ch.dissem.bitmessage.exports.MessageExport
import ch.dissem.bitmessage.utils.UnixTime 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.Libs
import com.mikepenz.aboutlibraries.LibsBuilder import com.mikepenz.aboutlibraries.LibsBuilder
import org.jetbrains.anko.doAsync import org.jetbrains.anko.doAsync
import org.jetbrains.anko.support.v4.indeterminateProgressDialog import org.jetbrains.anko.support.v4.indeterminateProgressDialog
import org.jetbrains.anko.uiThread import org.jetbrains.anko.uiThread
import org.slf4j.LoggerFactory
import java.io.File import java.io.File
import java.io.FileOutputStream import java.io.FileOutputStream
import java.util.zip.ZipEntry import java.util.zip.ZipEntry
import java.util.zip.ZipInputStream
import java.util.zip.ZipOutputStream import java.util.zip.ZipOutputStream
/** /**
* @author Christian Basler * @author Christian Basler
*/ */
@ -128,7 +132,7 @@ class SettingsFragment : PreferenceFragmentCompat(), SharedPreferences.OnSharedP
intent.type = "application/zip" intent.type = "application/zip"
intent.putExtra(Intent.EXTRA_SUBJECT, "abit-export.zip") intent.putExtra(Intent.EXTRA_SUBJECT, "abit-export.zip")
intent.putExtra(Intent.EXTRA_STREAM, contentUri) intent.putExtra(Intent.EXTRA_STREAM, contentUri)
startActivityForResult(Intent.createChooser(intent, ""), 0) startActivityForResult(Intent.createChooser(intent, ""), WRITE_EXPORT_REQUEST_CODE)
uiThread { uiThread {
dialog.dismiss() dialog.dismiss()
} }
@ -136,6 +140,15 @@ class SettingsFragment : PreferenceFragmentCompat(), SharedPreferences.OnSharedP
return@OnPreferenceClickListener true return@OnPreferenceClickListener true
} }
findPreference("import")?.onPreferenceClickListener = Preference.OnPreferenceClickListener {
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT)
intent.addCategory(Intent.CATEGORY_OPENABLE)
intent.type = "application/zip"
startActivityForResult(intent, READ_IMPORT_REQUEST_CODE)
return@OnPreferenceClickListener true
}
findPreference("status").onPreferenceClickListener = Preference.OnPreferenceClickListener { findPreference("status").onPreferenceClickListener = Preference.OnPreferenceClickListener {
val activity = activity as MainActivity val activity = activity as MainActivity
if (activity.hasDetailPane()) { if (activity.hasDetailPane()) {
@ -147,8 +160,57 @@ class SettingsFragment : PreferenceFragmentCompat(), SharedPreferences.OnSharedP
} }
} }
private fun processEntry(zipFile: Uri, entry: String, processor: (JsonArray<*>) -> Unit) {
ZipInputStream(context.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?) { override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
Preferences.cleanupExportDirectory(context) when (requestCode) {
WRITE_EXPORT_REQUEST_CODE -> Preferences.cleanupExportDirectory(context)
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 ctx = Singleton.getBitmessageContext(context)
val labels = mutableMapOf<String, Label>()
val zipFile = data.data
processEntry(zipFile, "contacts.json") { json ->
ContactExport.importContacts(json).forEach { contact ->
ctx.addresses.save(contact)
}
}
ctx.messages.getLabels().forEach { label ->
labels[label.toString()] = label
}
processEntry(zipFile, "labels.json") { json ->
MessageExport.importLabels(json).forEach { label ->
if (!labels.contains(label.toString())) {
ctx.messages.save(label)
labels[label.toString()] = label
}
}
}
processEntry(zipFile, "messages.json") { json ->
MessageExport.importMessages(json, labels).forEach { message ->
ctx.messages.save(message)
}
}
uiThread {
dialog.dismiss()
}
}
}
}
}
} }
override fun onAttach(ctx: Context?) { override fun onAttach(ctx: Context?) {
@ -182,4 +244,9 @@ class SettingsFragment : PreferenceFragmentCompat(), SharedPreferences.OnSharedP
} }
} }
} }
companion object {
const val WRITE_EXPORT_REQUEST_CODE = 1
const val READ_IMPORT_REQUEST_CODE = 2
}
} }

View File

@ -1,428 +0,0 @@
/*
* Copyright 2015 Christian Basler
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ch.dissem.apps.abit.repository;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.DatabaseUtils;
import android.database.sqlite.SQLiteConstraintException;
import android.database.sqlite.SQLiteDatabase;
import android.support.annotation.NonNull;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.ByteArrayInputStream;
import java.util.Collection;
import java.util.LinkedList;
import java.util.List;
import java.util.UUID;
import ch.dissem.apps.abit.util.Labels;
import ch.dissem.apps.abit.util.UuidUtils;
import ch.dissem.bitmessage.entity.BitmessageAddress;
import ch.dissem.bitmessage.entity.Plaintext;
import ch.dissem.bitmessage.entity.valueobject.InventoryVector;
import ch.dissem.bitmessage.entity.valueobject.Label;
import ch.dissem.bitmessage.ports.AbstractMessageRepository;
import ch.dissem.bitmessage.ports.MessageRepository;
import ch.dissem.bitmessage.utils.Encode;
import static ch.dissem.apps.abit.util.UuidUtils.asUuid;
import static ch.dissem.bitmessage.utils.Strings.hex;
import static java.lang.String.valueOf;
/**
* {@link MessageRepository} implementation using the Android SQL API.
*/
public class AndroidMessageRepository extends AbstractMessageRepository {
private static final Logger LOG = LoggerFactory.getLogger(AndroidMessageRepository.class);
public static final Label LABEL_ARCHIVE = new Label("archive", null, 0);
private static final String TABLE_NAME = "Message";
private static final String COLUMN_ID = "id";
private static final String COLUMN_IV = "iv";
private static final String COLUMN_TYPE = "type";
private static final String COLUMN_SENDER = "sender";
private static final String COLUMN_RECIPIENT = "recipient";
private static final String COLUMN_DATA = "data";
private static final String COLUMN_ACK_DATA = "ack_data";
private static final String COLUMN_SENT = "sent";
private static final String COLUMN_RECEIVED = "received";
private static final String COLUMN_STATUS = "status";
private static final String COLUMN_TTL = "ttl";
private static final String COLUMN_RETRIES = "retries";
private static final String COLUMN_NEXT_TRY = "next_try";
private static final String COLUMN_INITIAL_HASH = "initial_hash";
private static final String COLUMN_CONVERSATION = "conversation";
private static final String PARENTS_TABLE_NAME = "Message_Parent";
private static final String JOIN_TABLE_NAME = "Message_Label";
private static final String JT_COLUMN_MESSAGE = "message_id";
private static final String JT_COLUMN_LABEL = "label_id";
private static final String LBL_TABLE_NAME = "Label";
private static final String LBL_COLUMN_ID = "id";
private static final String LBL_COLUMN_LABEL = "label";
private static final String LBL_COLUMN_TYPE = "type";
private static final String LBL_COLUMN_COLOR = "color";
private static final String LBL_COLUMN_ORDER = "ord";
private final SqlHelper sql;
private final Context context;
public AndroidMessageRepository(SqlHelper sql, Context ctx) {
this.sql = sql;
this.context = ctx;
}
@NonNull
@Override
public List<Plaintext> findMessages(Label label) {
if (label == LABEL_ARCHIVE) {
return super.findMessages((Label) null);
} else {
return super.findMessages(label);
}
}
@NonNull
public List<Long> findMessageIds(Label label) {
if (label == LABEL_ARCHIVE) {
return findIds("id NOT IN (SELECT message_id FROM Message_Label)");
} else {
return findIds("id IN (SELECT message_id FROM Message_Label WHERE label_id=" + label.getId() + ")");
}
}
@NonNull
public List<Label> findLabels(String where) {
List<Label> result = new LinkedList<>();
// Define a projection that specifies which columns from the database
// you will actually use after this query.
String[] projection = {
LBL_COLUMN_ID,
LBL_COLUMN_LABEL,
LBL_COLUMN_TYPE,
LBL_COLUMN_COLOR
};
SQLiteDatabase db = sql.getReadableDatabase();
try (Cursor c = db.query(
LBL_TABLE_NAME, projection,
where,
null, null, null,
LBL_COLUMN_ORDER
)) {
while (c.moveToNext()) {
result.add(getLabel(c));
}
}
return result;
}
private Label getLabel(Cursor c) {
String typeName = c.getString(c.getColumnIndex(LBL_COLUMN_TYPE));
Label.Type type = typeName == null ? null : Label.Type.valueOf(typeName);
String text = Labels.getText(type, null, context);
if (text == null) {
text = c.getString(c.getColumnIndex(LBL_COLUMN_LABEL));
}
Label label = new Label(
text,
type,
c.getInt(c.getColumnIndex(LBL_COLUMN_COLOR)));
label.setId(c.getLong(c.getColumnIndex(LBL_COLUMN_ID)));
return label;
}
@Override
public int countUnread(Label label) {
if (label == LABEL_ARCHIVE) {
return 0;
} else if (label == null) {
return (int) DatabaseUtils.queryNumEntries(
sql.getReadableDatabase(),
TABLE_NAME,
"id IN (SELECT message_id FROM Message_Label WHERE label_id IN (SELECT id FROM Label WHERE type=?))",
new String[]{
String.valueOf(label.getId()),
Label.Type.UNREAD.name()
}
);
} else {
return (int) DatabaseUtils.queryNumEntries(
sql.getReadableDatabase(),
TABLE_NAME,
" id IN (SELECT message_id FROM Message_Label WHERE label_id=?) " +
"AND id IN (SELECT message_id FROM Message_Label WHERE label_id IN (SELECT id FROM Label WHERE type=?))",
new String[]{
String.valueOf(label.getId()),
Label.Type.UNREAD.name()
}
);
}
}
@NonNull
@Override
public List<UUID> findConversations(Label label) {
String[] projection = {
COLUMN_CONVERSATION,
};
String where;
if (label == null) {
where = "id NOT IN (SELECT message_id FROM Message_Label)";
} else {
where = "id IN (SELECT message_id FROM Message_Label WHERE label_id=" + label.getId() + ")";
}
List<UUID> result = new LinkedList<>();
SQLiteDatabase db = sql.getReadableDatabase();
try (Cursor c = db.query(
TABLE_NAME, projection,
where,
null, null, null, null
)) {
while (c.moveToNext()) {
byte[] uuidBytes = c.getBlob(c.getColumnIndex(COLUMN_CONVERSATION));
result.add(asUuid(uuidBytes));
}
}
return result;
}
private void updateParents(SQLiteDatabase db, Plaintext message) {
if (message.getInventoryVector() == null || message.getParents().isEmpty()) {
// There are no parents to save yet (they are saved in the extended data, that's enough for now)
return;
}
byte[] childIV = message.getInventoryVector().getHash();
db.delete(PARENTS_TABLE_NAME, "child=?", new String[]{hex(childIV)});
// save new parents
int order = 0;
for (InventoryVector parentIV : message.getParents()) {
Plaintext parent = getMessage(parentIV);
if (parent != null) {
mergeConversations(db, parent.getConversationId(), message.getConversationId());
order++;
ContentValues values = new ContentValues();
values.put("parent", parentIV.getHash());
values.put("child", childIV);
values.put("pos", order);
values.put("conversation", UuidUtils.asBytes(message.getConversationId()));
db.insertOrThrow(PARENTS_TABLE_NAME, null, values);
}
}
}
/**
* Replaces every occurrence of the source conversation ID with the target ID
*
* @param db is used to keep everything within one transaction
* @param source ID of the conversation to be merged
* @param target ID of the merge target
*/
private void mergeConversations(SQLiteDatabase db, UUID source, UUID target) {
ContentValues values = new ContentValues();
values.put("conversation", UuidUtils.asBytes(target));
String[] whereArgs = {hex(UuidUtils.asBytes(source))};
db.update(TABLE_NAME, values, "conversation=?", whereArgs);
db.update(PARENTS_TABLE_NAME, values, "conversation=?", whereArgs);
}
@NonNull
private List<Long> findIds(String where) {
List<Long> result = new LinkedList<>();
// Define a projection that specifies which columns from the database
// you will actually use after this query.
String[] projection = {
COLUMN_ID
};
SQLiteDatabase db = sql.getReadableDatabase();
try (Cursor c = db.query(
TABLE_NAME, projection,
where,
null, null, null,
COLUMN_RECEIVED + " DESC, " + COLUMN_SENT + " DESC"
)) {
while (c.moveToNext()) {
long id = c.getLong(0);
result.add(id);
}
}
return result;
}
@NonNull
protected List<Plaintext> find(String where) {
List<Plaintext> result = new LinkedList<>();
// Define a projection that specifies which columns from the database
// you will actually use after this query.
String[] projection = {
COLUMN_ID,
COLUMN_IV,
COLUMN_TYPE,
COLUMN_SENDER,
COLUMN_RECIPIENT,
COLUMN_DATA,
COLUMN_ACK_DATA,
COLUMN_SENT,
COLUMN_RECEIVED,
COLUMN_STATUS,
COLUMN_TTL,
COLUMN_RETRIES,
COLUMN_NEXT_TRY,
COLUMN_CONVERSATION
};
SQLiteDatabase db = sql.getReadableDatabase();
try (Cursor c = db.query(
TABLE_NAME, projection,
where,
null, null, null,
COLUMN_RECEIVED + " DESC, " + COLUMN_SENT + " DESC"
)) {
while (c.moveToNext()) {
byte[] iv = c.getBlob(c.getColumnIndex(COLUMN_IV));
byte[] data = c.getBlob(c.getColumnIndex(COLUMN_DATA));
Plaintext.Type type = Plaintext.Type.valueOf(c.getString(c.getColumnIndex
(COLUMN_TYPE)));
Plaintext.Builder builder = Plaintext.readWithoutSignature(type,
new ByteArrayInputStream(data));
long id = c.getLong(c.getColumnIndex(COLUMN_ID));
builder.id(id);
builder.IV(InventoryVector.fromHash(iv));
String sender = c.getString(c.getColumnIndex(COLUMN_SENDER));
if (sender != null) {
BitmessageAddress address = ctx.getAddressRepository().getAddress(sender);
if (address != null) {
builder.from(address);
} else {
builder.from(new BitmessageAddress(sender));
}
}
String recipient = c.getString(c.getColumnIndex(COLUMN_RECIPIENT));
if (recipient != null) {
BitmessageAddress address = ctx.getAddressRepository().getAddress(recipient);
if (address != null) {
builder.to(address);
} else {
builder.to(new BitmessageAddress(sender));
}
}
builder.ackData(c.getBlob(c.getColumnIndex(COLUMN_ACK_DATA)));
builder.sent(c.getLong(c.getColumnIndex(COLUMN_SENT)));
builder.received(c.getLong(c.getColumnIndex(COLUMN_RECEIVED)));
builder.status(Plaintext.Status.valueOf(c.getString(c.getColumnIndex
(COLUMN_STATUS))));
builder.ttl(c.getLong(c.getColumnIndex(COLUMN_TTL)));
builder.retries(c.getInt(c.getColumnIndex(COLUMN_RETRIES)));
int nextTryColumn = c.getColumnIndex(COLUMN_NEXT_TRY);
if (!c.isNull(nextTryColumn)) {
builder.nextTry(c.getLong(nextTryColumn));
}
builder.conversation(asUuid(c.getBlob(c.getColumnIndex(COLUMN_CONVERSATION))));
builder.labels(findLabels(id));
result.add(builder.build());
}
}
return result;
}
private Collection<Label> findLabels(long id) {
return findLabels("id IN (SELECT label_id FROM Message_Label WHERE message_id=" + id + ")");
}
@Override
public void save(Plaintext message) {
saveContactIfNecessary(message.getFrom());
saveContactIfNecessary(message.getTo());
SQLiteDatabase db = sql.getWritableDatabase();
try {
db.beginTransaction();
// save message
if (message.getId() == null) {
insert(db, message);
} else {
update(db, message);
}
updateParents(db, message);
// remove existing labels
db.delete(JOIN_TABLE_NAME, "message_id=?", new String[]{valueOf(message.getId())});
// save labels
ContentValues values = new ContentValues();
for (Label label : message.getLabels()) {
values.put(JT_COLUMN_LABEL, (Long) label.getId());
values.put(JT_COLUMN_MESSAGE, (Long) message.getId());
db.insertOrThrow(JOIN_TABLE_NAME, null, values);
}
db.setTransactionSuccessful();
} catch (SQLiteConstraintException e) {
LOG.trace(e.getMessage(), e);
} finally {
db.endTransaction();
}
}
private ContentValues getValues(Plaintext message) {
ContentValues values = new ContentValues();
values.put(COLUMN_IV, message.getInventoryVector() == null ? null : message
.getInventoryVector().getHash());
values.put(COLUMN_TYPE, message.getType().name());
values.put(COLUMN_SENDER, message.getFrom().getAddress());
values.put(COLUMN_RECIPIENT, message.getTo() == null ? null : message.getTo().getAddress());
values.put(COLUMN_DATA, Encode.bytes(message));
values.put(COLUMN_ACK_DATA, message.getAckData());
values.put(COLUMN_SENT, message.getSent());
values.put(COLUMN_RECEIVED, message.getReceived());
values.put(COLUMN_STATUS, message.getStatus().name());
values.put(COLUMN_INITIAL_HASH, message.getInitialHash());
values.put(COLUMN_TTL, message.getTTL());
values.put(COLUMN_RETRIES, message.getRetries());
values.put(COLUMN_NEXT_TRY, message.getNextTry());
values.put(COLUMN_CONVERSATION, UuidUtils.asBytes(message.getConversationId()));
return values;
}
private void insert(SQLiteDatabase db, Plaintext message) {
long id = db.insertOrThrow(TABLE_NAME, null, getValues(message));
message.setId(id);
}
private void update(SQLiteDatabase db, Plaintext message) {
db.update(TABLE_NAME, getValues(message), "id=?", new String[]{valueOf(message.getId())});
}
@Override
public void remove(Plaintext message) {
SQLiteDatabase db = sql.getWritableDatabase();
db.delete(TABLE_NAME, "id = " + message.getId(), null);
}
}

View File

@ -0,0 +1,403 @@
/*
* Copyright 2015 Christian Basler
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ch.dissem.apps.abit.repository
import android.content.ContentValues
import android.content.Context
import android.database.Cursor
import android.database.DatabaseUtils
import android.database.sqlite.SQLiteConstraintException
import android.database.sqlite.SQLiteDatabase
import ch.dissem.apps.abit.util.Labels
import ch.dissem.apps.abit.util.UuidUtils
import ch.dissem.apps.abit.util.UuidUtils.asUuid
import ch.dissem.bitmessage.entity.BitmessageAddress
import ch.dissem.bitmessage.entity.Plaintext
import ch.dissem.bitmessage.entity.valueobject.InventoryVector
import ch.dissem.bitmessage.entity.valueobject.Label
import ch.dissem.bitmessage.ports.AbstractMessageRepository
import ch.dissem.bitmessage.ports.MessageRepository
import ch.dissem.bitmessage.utils.Encode
import ch.dissem.bitmessage.utils.Strings.hex
import org.slf4j.LoggerFactory
import java.io.ByteArrayInputStream
import java.util.*
/**
* [MessageRepository] implementation using the Android SQL API.
*/
class AndroidMessageRepository(private val sql: SqlHelper, private val context: Context) : AbstractMessageRepository() {
override fun findMessages(label: Label?): List<Plaintext> {
if (label === LABEL_ARCHIVE) {
return super.findMessages(null as Label?)
} else {
return super.findMessages(label)
}
}
fun findMessageIds(label: Label): List<Long> {
if (label === LABEL_ARCHIVE) {
return findIds("id NOT IN (SELECT message_id FROM Message_Label)")
} else {
return findIds("id IN (SELECT message_id FROM Message_Label WHERE label_id=" + label.id + ")")
}
}
public override fun findLabels(where: String): List<Label> {
val result = LinkedList<Label>()
// Define a projection that specifies which columns from the database
// you will actually use after this query.
val projection = arrayOf(LBL_COLUMN_ID, LBL_COLUMN_LABEL, LBL_COLUMN_TYPE, LBL_COLUMN_COLOR)
val db = sql.readableDatabase
db.query(
LBL_TABLE_NAME, projection,
where, null, null, null,
LBL_COLUMN_ORDER
).use { c ->
while (c.moveToNext()) {
result.add(getLabel(c))
}
}
return result
}
private fun getLabel(c: Cursor): Label {
val typeName = c.getString(c.getColumnIndex(LBL_COLUMN_TYPE))
val type = if (typeName == null) null else Label.Type.valueOf(typeName)
val text: String? = Labels.getText(type, null, context)
val label = Label(
text ?: c.getString(c.getColumnIndex(LBL_COLUMN_LABEL)),
type,
c.getInt(c.getColumnIndex(LBL_COLUMN_COLOR)))
label.id = c.getLong(c.getColumnIndex(LBL_COLUMN_ID))
return label
}
override fun save(label: Label) {
val db = sql.writableDatabase
if (label.id != null) {
val values = ContentValues()
values.put(LBL_COLUMN_LABEL, label.toString())
values.put(LBL_COLUMN_TYPE, label.type?.name)
values.put(LBL_COLUMN_COLOR, label.color)
values.put(LBL_COLUMN_ORDER, label.ord)
db.update(LBL_TABLE_NAME, values, "id=?", arrayOf(label.id.toString()))
} else {
try {
db.beginTransaction()
val exists = DatabaseUtils.queryNumEntries(db, LBL_TABLE_NAME, "label=?", arrayOf(label.toString())) > 0
if (exists) {
val values = ContentValues()
values.put(LBL_COLUMN_TYPE, label.type?.name)
values.put(LBL_COLUMN_COLOR, label.color)
values.put(LBL_COLUMN_ORDER, label.ord)
db.update(LBL_TABLE_NAME, values, "label=?", arrayOf(label.toString()))
} else {
val values = ContentValues()
values.put(LBL_COLUMN_LABEL, label.toString())
values.put(LBL_COLUMN_TYPE, label.type?.name)
values.put(LBL_COLUMN_COLOR, label.color)
values.put(LBL_COLUMN_ORDER, label.ord)
db.insertOrThrow(LBL_TABLE_NAME, null, values)
}
db.setTransactionSuccessful()
} finally {
db.endTransaction()
}
}
}
override fun countUnread(label: Label?): Int {
if (label === LABEL_ARCHIVE) {
return 0
} else if (label == null) {
return DatabaseUtils.queryNumEntries(
sql.readableDatabase,
TABLE_NAME,
"id IN (SELECT message_id FROM Message_Label WHERE label_id IN (SELECT id FROM Label WHERE type=?))",
arrayOf(Label.Type.UNREAD.name)
).toInt()
} else {
return DatabaseUtils.queryNumEntries(
sql.readableDatabase,
TABLE_NAME,
" id IN (SELECT message_id FROM Message_Label WHERE label_id=?) " + "AND id IN (SELECT message_id FROM Message_Label WHERE label_id IN (SELECT id FROM Label WHERE type=?))",
arrayOf(label.id.toString(), Label.Type.UNREAD.name)
).toInt()
}
}
override fun findConversations(label: Label?): List<UUID> {
val projection = arrayOf(COLUMN_CONVERSATION)
val where: String
if (label == null) {
where = "id NOT IN (SELECT message_id FROM Message_Label)"
} else {
where = "id IN (SELECT message_id FROM Message_Label WHERE label_id=" + label.id + ")"
}
val result = LinkedList<UUID>()
val db = sql.readableDatabase
db.query(
TABLE_NAME, projection,
where, null, null, null, null
).use { c ->
while (c.moveToNext()) {
val uuidBytes = c.getBlob(c.getColumnIndex(COLUMN_CONVERSATION))
result.add(asUuid(uuidBytes))
}
}
return result
}
private fun updateParents(db: SQLiteDatabase, message: Plaintext) {
if (message.inventoryVector == null || message.parents.isEmpty()) {
// There are no parents to save yet (they are saved in the extended data, that's enough for now)
return
}
val childIV = message.inventoryVector!!.hash
db.delete(PARENTS_TABLE_NAME, "child=?", arrayOf(hex(childIV)))
// save new parents
var order = 0
for (parentIV in message.parents) {
val parent = getMessage(parentIV)
if (parent != null) {
mergeConversations(db, parent.conversationId, message.conversationId)
order++
val values = ContentValues()
values.put("parent", parentIV.hash)
values.put("child", childIV)
values.put("pos", order)
values.put("conversation", UuidUtils.asBytes(message.conversationId))
db.insertOrThrow(PARENTS_TABLE_NAME, null, values)
}
}
}
/**
* Replaces every occurrence of the source conversation ID with the target ID
* @param db is used to keep everything within one transaction
* *
* @param source ID of the conversation to be merged
* *
* @param target ID of the merge target
*/
private fun mergeConversations(db: SQLiteDatabase, source: UUID, target: UUID) {
val values = ContentValues()
values.put("conversation", UuidUtils.asBytes(target))
val whereArgs = arrayOf(hex(UuidUtils.asBytes(source)))
db.update(TABLE_NAME, values, "conversation=?", whereArgs)
db.update(PARENTS_TABLE_NAME, values, "conversation=?", whereArgs)
}
private fun findIds(where: String): List<Long> {
val result = LinkedList<Long>()
// Define a projection that specifies which columns from the database
// you will actually use after this query.
val projection = arrayOf(COLUMN_ID)
val db = sql.readableDatabase
db.query(
TABLE_NAME, projection,
where, null, null, null,
"$COLUMN_RECEIVED DESC, $COLUMN_SENT DESC"
).use { c ->
while (c.moveToNext()) {
val id = c.getLong(0)
result.add(id)
}
}
return result
}
override fun find(where: String): List<Plaintext> {
val result = LinkedList<Plaintext>()
// Define a projection that specifies which columns from the database
// you will actually use after this query.
val projection = arrayOf(COLUMN_ID, COLUMN_IV, COLUMN_TYPE, COLUMN_SENDER, COLUMN_RECIPIENT, COLUMN_DATA, COLUMN_ACK_DATA, COLUMN_SENT, COLUMN_RECEIVED, COLUMN_STATUS, COLUMN_TTL, COLUMN_RETRIES, COLUMN_NEXT_TRY, COLUMN_CONVERSATION)
val db = sql.readableDatabase
db.query(
TABLE_NAME, projection,
where, null, null, null,
"$COLUMN_RECEIVED DESC, $COLUMN_SENT DESC"
).use { c ->
while (c.moveToNext()) {
val iv = c.getBlob(c.getColumnIndex(COLUMN_IV))
val data = c.getBlob(c.getColumnIndex(COLUMN_DATA))
val type = Plaintext.Type.valueOf(c.getString(c.getColumnIndex(COLUMN_TYPE)))
val builder = Plaintext.readWithoutSignature(type,
ByteArrayInputStream(data))
val id = c.getLong(c.getColumnIndex(COLUMN_ID))
builder.id(id)
builder.IV(InventoryVector.fromHash(iv))
val sender = c.getString(c.getColumnIndex(COLUMN_SENDER))
if (sender != null) {
val address = ctx.addressRepository.getAddress(sender)
if (address != null) {
builder.from(address)
} else {
builder.from(BitmessageAddress(sender))
}
}
val recipient = c.getString(c.getColumnIndex(COLUMN_RECIPIENT))
if (recipient != null) {
val address = ctx.addressRepository.getAddress(recipient)
if (address != null) {
builder.to(address)
} else {
builder.to(BitmessageAddress(sender))
}
}
builder.ackData(c.getBlob(c.getColumnIndex(COLUMN_ACK_DATA)))
builder.sent(c.getLong(c.getColumnIndex(COLUMN_SENT)))
builder.received(c.getLong(c.getColumnIndex(COLUMN_RECEIVED)))
builder.status(Plaintext.Status.valueOf(c.getString(c.getColumnIndex(COLUMN_STATUS))))
builder.ttl(c.getLong(c.getColumnIndex(COLUMN_TTL)))
builder.retries(c.getInt(c.getColumnIndex(COLUMN_RETRIES)))
val nextTryColumn = c.getColumnIndex(COLUMN_NEXT_TRY)
if (!c.isNull(nextTryColumn)) {
builder.nextTry(c.getLong(nextTryColumn))
}
builder.conversation(asUuid(c.getBlob(c.getColumnIndex(COLUMN_CONVERSATION))))
builder.labels(findLabels(id))
result.add(builder.build())
}
}
return result
}
private fun findLabels(id: Long) = findLabels("id IN (SELECT label_id FROM Message_Label WHERE message_id=$id)")
override fun save(message: Plaintext) {
saveContactIfNecessary(message.from)
saveContactIfNecessary(message.to)
val db = sql.writableDatabase
try {
db.beginTransaction()
// save message
if (message.id == null) {
insert(db, message)
} else {
update(db, message)
}
updateParents(db, message)
// remove existing labels
db.delete(JOIN_TABLE_NAME, "message_id=?", arrayOf(message.id.toString()))
// save labels
val values = ContentValues()
for (label in message.labels) {
values.put(JT_COLUMN_LABEL, label.id as Long?)
values.put(JT_COLUMN_MESSAGE, message.id as Long?)
db.insertOrThrow(JOIN_TABLE_NAME, null, values)
}
db.setTransactionSuccessful()
} catch (e: SQLiteConstraintException) {
LOG.trace(e.message, e)
} finally {
db.endTransaction()
}
}
private fun getValues(message: Plaintext): ContentValues {
val values = ContentValues()
values.put(COLUMN_IV, if (message.inventoryVector == null)
null
else
message
.inventoryVector!!.hash)
values.put(COLUMN_TYPE, message.type.name)
values.put(COLUMN_SENDER, message.from.address)
values.put(COLUMN_RECIPIENT, if (message.to == null) null else message.to!!.address)
values.put(COLUMN_DATA, Encode.bytes(message))
values.put(COLUMN_ACK_DATA, message.ackData)
values.put(COLUMN_SENT, message.sent)
values.put(COLUMN_RECEIVED, message.received)
values.put(COLUMN_STATUS, message.status.name)
values.put(COLUMN_INITIAL_HASH, message.initialHash)
values.put(COLUMN_TTL, message.ttl)
values.put(COLUMN_RETRIES, message.retries)
values.put(COLUMN_NEXT_TRY, message.nextTry)
values.put(COLUMN_CONVERSATION, UuidUtils.asBytes(message.conversationId))
return values
}
private fun insert(db: SQLiteDatabase, message: Plaintext) {
val id = db.insertOrThrow(TABLE_NAME, null, getValues(message))
message.id = id
}
private fun update(db: SQLiteDatabase, message: Plaintext) {
db.update(TABLE_NAME, getValues(message), "id=?", arrayOf(message.id.toString()))
}
override fun remove(message: Plaintext) {
val db = sql.writableDatabase
db.delete(TABLE_NAME, "id = " + message.id!!, null)
}
companion object {
private val LOG = LoggerFactory.getLogger(AndroidMessageRepository::class.java)
@JvmField
val LABEL_ARCHIVE = Label("archive", null, 0)
private const val TABLE_NAME = "Message"
private const val COLUMN_ID = "id"
private const val COLUMN_IV = "iv"
private const val COLUMN_TYPE = "type"
private const val COLUMN_SENDER = "sender"
private const val COLUMN_RECIPIENT = "recipient"
private const val COLUMN_DATA = "data"
private const val COLUMN_ACK_DATA = "ack_data"
private const val COLUMN_SENT = "sent"
private const val COLUMN_RECEIVED = "received"
private const val COLUMN_STATUS = "status"
private const val COLUMN_TTL = "ttl"
private const val COLUMN_RETRIES = "retries"
private const val COLUMN_NEXT_TRY = "next_try"
private const val COLUMN_INITIAL_HASH = "initial_hash"
private const val COLUMN_CONVERSATION = "conversation"
private const val PARENTS_TABLE_NAME = "Message_Parent"
private const val JOIN_TABLE_NAME = "Message_Label"
private const val JT_COLUMN_MESSAGE = "message_id"
private const val JT_COLUMN_LABEL = "label_id"
private const val LBL_TABLE_NAME = "Label"
private const val LBL_COLUMN_ID = "id"
private const val LBL_COLUMN_LABEL = "label"
private const val LBL_COLUMN_TYPE = "type"
private const val LBL_COLUMN_COLOR = "color"
private const val LBL_COLUMN_ORDER = "ord"
}
}

View File

@ -120,6 +120,8 @@ As an alternative you could configure a trusted node in the settings, but as of
<string name="error_msg_recipient_missing">Please set a recipient</string> <string name="error_msg_recipient_missing">Please set a recipient</string>
<string name="export_data">Export</string> <string name="export_data">Export</string>
<string name="export_data_summary">Export all messages and contacts (but not identities)</string> <string name="export_data_summary">Export all messages and contacts (but not identities)</string>
<string name="import_data">Import</string>
<string name="import_data_summary">Import messages and contacts (but not identities)</string>
<string name="request_acknowledgements">Request acknowledgements</string> <string name="request_acknowledgements">Request acknowledgements</string>
<string name="request_acknowledgements_summary">Acknowledges allow making sure a message was received, but require additional time to send</string> <string name="request_acknowledgements_summary">Acknowledges allow making sure a message was received, but require additional time to send</string>
<string name="got_it">Got it</string> <string name="got_it">Got it</string>

View File

@ -51,6 +51,11 @@
android:title="@string/export_data" android:title="@string/export_data"
android:summary="@string/export_data_summary" android:summary="@string/export_data_summary"
/> />
<Preference
android:key="import"
android:title="@string/import_data"
android:summary="@string/import_data_summary"
/>
<Preference <Preference
android:key="status" android:key="status"
android:summary="@string/status_summary" android:summary="@string/status_summary"