Abit/app/src/main/java/ch/dissem/apps/abit/repository/AndroidMessageRepository.kt

362 lines
15 KiB
Kotlin

/*
* 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.AlreadyStoredException
import ch.dissem.bitmessage.ports.MessageRepository
import ch.dissem.bitmessage.utils.Encode
import ch.dissem.bitmessage.utils.Strings.hex
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?, offset: Int, limit: Int) = if (label === LABEL_ARCHIVE) {
super.findMessages(null as Label?, offset, limit)
} else {
super.findMessages(label, offset, limit)
}
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)
sql.readableDatabase.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?) = when {
label === LABEL_ARCHIVE -> 0
label == null -> 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 -> 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 = when {
label === LABEL_ARCHIVE -> "id NOT IN (SELECT message_id FROM Message_Label)"
label == null -> null
else -> "id IN (SELECT message_id FROM Message_Label WHERE label_id=${label.id})"
}
val result = LinkedList<UUID>()
sql.readableDatabase.query(
true,
TABLE_NAME, projection, where,
null, 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) {
val inventoryVector = message.inventoryVector
if (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 = inventoryVector.hash
db.delete(PARENTS_TABLE_NAME, "child=?", arrayOf(hex(childIV)))
// save new parents
var order = 0
for (parentIV in message.parents) {
getMessage(parentIV)?.let { parent ->
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 where = "conversation=X'${hex(UuidUtils.asBytes(source))}'"
db.update(TABLE_NAME, values, where, null)
db.update(PARENTS_TABLE_NAME, values, where, null)
}
override fun find(where: String, offset: Int, limit: Int): 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",
if (limit == 0) null else "$offset, $limit"
).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) {
throw AlreadyStoredException(cause = e)
} finally {
db.endTransaction()
}
}
private fun getValues(message: Plaintext): ContentValues {
val values = ContentValues()
values.put(COLUMN_IV, message.inventoryVector?.hash)
values.put(COLUMN_TYPE, message.type.name)
values.put(COLUMN_SENDER, message.from.address)
values.put(COLUMN_RECIPIENT, 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 = ?", arrayOf(message.id.toString()))
}
companion object {
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"
}
}