Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Contact pairing #42

Merged
merged 37 commits into from
Sep 12, 2023
Merged
Show file tree
Hide file tree
Changes from 35 commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
e6b0651
initial commit with a lot of functionality done
GabeGiro Jul 19, 2023
250f0d6
functionality done without running the app
GabeGiro Jul 20, 2023
eab5bb8
updated a lot. next is migrating database
GabeGiro Jul 20, 2023
43e69d5
updated minor typos and commented some unused code as potential idea …
GabeGiro Jul 20, 2023
320054f
added fallbackToDestructiveMigrationOnDowngrade
GabeGiro Jul 20, 2023
f23c5e2
updated the flow based on few conditions
GabeGiro Jul 20, 2023
913bbbd
updated PairingMatchDataModel and removed switched values in 2 places
GabeGiro Jul 21, 2023
39f267b
updated 1 comment
GabeGiro Jul 21, 2023
b838831
updated one comment
GabeGiro Jul 21, 2023
e8e356a
removed PublicKey from contact entity
GabeGiro Jul 21, 2023
5da4ef0
updated the authorization received
GabeGiro Jul 21, 2023
52851d6
fixed few bugs
GabeGiro Jul 21, 2023
84507fa
optimized sending the pairing request
GabeGiro Jul 21, 2023
bcdef51
removed some comments and added TODOs
GabeGiro Jul 21, 2023
3814924
replaced all address instances with veraId
GabeGiro Jul 21, 2023
1fbb9d9
updated so that receiving messages is only started when the gateway i…
GabeGiro Jul 24, 2023
09a0baf
made sure the messages are received only once and made one part of th…
GabeGiro Jul 24, 2023
bb3f344
updated so that we dont save multiple same contact veraIds
GabeGiro Jul 24, 2023
97b72fd
added few check just to make sure that once the contact is authorized…
GabeGiro Jul 24, 2023
af717bf
setup project and theme properties + splash theme
migulyaev Sep 5, 2023
4c98ce5
registration + waiting for registration + pair with others screens
migulyaev Sep 8, 2023
67e6f39
registration screen ui fixes
migulyaev Sep 8, 2023
12fb7fa
code refactoring
migulyaev Sep 8, 2023
e27ec91
move deprecated code to app-old module
migulyaev Sep 8, 2023
bd5d46a
design fixes
migulyaev Sep 8, 2023
8091693
share id feature
migulyaev Sep 8, 2023
4ebf076
design fixes
migulyaev Sep 8, 2023
63b1a8c
fixes after refactoring
migulyaev Sep 8, 2023
3709c6f
design fix
migulyaev Sep 8, 2023
5aaa659
AwalaManager flow -> channel fix + extract parser of messages to a se…
migulyaev Sep 10, 2023
dcda647
fix linter
migulyaev Sep 10, 2023
eeb5a45
contact pairing
migulyaev Sep 11, 2023
a52afbe
process incoming messages in-band
migulyaev Sep 11, 2023
17938fc
Merge branch 'in-band-message-processing' into contact_pairing
migulyaev Sep 11, 2023
241125c
allow sent multiple contact request / hints on PairWithOthersScreen /…
migulyaev Sep 12, 2023
46da2ad
merge main -> contact_pairing
migulyaev Sep 12, 2023
0838359
fix inserting the same contact multiple times
migulyaev Sep 12, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions app/schemas/tech.relaycorp.letro.storage.LetroDatabase/1.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"formatVersion": 1,
"database": {
"version": 1,
"identityHash": "f727ada9d0ee39552687eb25fef6f248",
"identityHash": "d5edc6419007841d2365ac43dd8af292",
"entities": [
{
"tableName": "account",
Expand Down Expand Up @@ -38,7 +38,7 @@
},
{
"tableName": "contacts",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `ownerVeraId` TEXT NOT NULL, `contactVeraId` TEXT NOT NULL, `alias` TEXT, `contactEndpointId` TEXT, `status` TEXT NOT NULL, FOREIGN KEY(`ownerVeraId`) REFERENCES `account`(`veraId`) ON UPDATE NO ACTION ON DELETE CASCADE )",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `ownerVeraId` TEXT NOT NULL, `contactVeraId` TEXT NOT NULL, `alias` TEXT, `contactEndpointId` TEXT, `status` INTEGER NOT NULL, FOREIGN KEY(`ownerVeraId`) REFERENCES `account`(`veraId`) ON UPDATE NO ACTION ON DELETE CASCADE )",
"fields": [
{
"fieldPath": "id",
Expand Down Expand Up @@ -73,7 +73,7 @@
{
"fieldPath": "status",
"columnName": "status",
"affinity": "TEXT",
"affinity": "INTEGER",
"notNull": true
}
],
Expand Down Expand Up @@ -112,7 +112,7 @@
"views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'f727ada9d0ee39552687eb25fef6f248')"
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'd5edc6419007841d2365ac43dd8af292')"
]
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.launch
import tech.relaycorp.letro.account.model.Account
import javax.inject.Inject
Expand All @@ -21,7 +22,7 @@ class AccountRepositoryImpl @Inject constructor(

private val databaseScope: CoroutineScope = CoroutineScope(Dispatchers.IO)
private val _allAccounts = MutableSharedFlow<List<Account>>()
private val _currentAccount = MutableSharedFlow<Account?>()
private val _currentAccount = MutableStateFlow<Account?>(null)
override val currentAccount: Flow<Account?>
get() = _currentAccount

Expand Down
52 changes: 34 additions & 18 deletions app/src/main/java/tech/relaycorp/letro/awala/AwalaManager.kt
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package tech.relaycorp.letro.awala

import android.content.Context
import android.util.Base64
import android.util.Log
import androidx.annotation.RawRes
import dagger.hilt.android.qualifiers.ApplicationContext
Expand All @@ -9,9 +10,6 @@ import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.Job
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.launch
import kotlinx.coroutines.newSingleThreadContext
import kotlinx.coroutines.withContext
Expand All @@ -24,31 +22,34 @@ import tech.relaycorp.awaladroid.endpoint.PublicThirdPartyEndpoint
import tech.relaycorp.awaladroid.endpoint.ThirdPartyEndpoint
import tech.relaycorp.awaladroid.messaging.OutgoingMessage
import tech.relaycorp.letro.R
import tech.relaycorp.letro.awala.message.AwalaIncomingMessage
import tech.relaycorp.letro.awala.message.AwalaOutgoingMessage
import tech.relaycorp.letro.awala.message.MessageRecipient
import tech.relaycorp.letro.awala.message.MessageType
import tech.relaycorp.letro.awala.parser.AwalaMessageParser
import tech.relaycorp.letro.awala.processor.AwalaMessageProcessor
import tech.relaycorp.letro.ui.navigation.Route
import tech.relaycorp.letro.utils.awala.loadNonNullFirstPartyEndpoint
import tech.relaycorp.letro.utils.awala.loadNonNullThirdPartyEndpoint
import java.util.concurrent.atomic.AtomicBoolean
import javax.inject.Inject

interface AwalaManager {
val incomingMessages: Flow<AwalaIncomingMessage<*>>
suspend fun sendMessage(
outgoingMessage: AwalaOutgoingMessage,
recipient: MessageRecipient,
)
suspend fun isAwalaInstalled(currentScreen: Route): Boolean
suspend fun authorizeUsers(
// TODO: after MVP handle several first party endpoints
thirdPartyPublicKey: ByteArray,
)
suspend fun getFirstPartyPublicKey(): String
}

@OptIn(ExperimentalCoroutinesApi::class)
class AwalaManagerImpl @Inject constructor(
private val awalaRepository: AwalaRepository,
@ApplicationContext private val context: Context,
private val parser: AwalaMessageParser,
private val processor: AwalaMessageProcessor,
) : AwalaManager {

private val awalaScope = CoroutineScope(Dispatchers.IO)
Expand All @@ -59,10 +60,6 @@ class AwalaManagerImpl @Inject constructor(
@OptIn(DelicateCoroutinesApi::class)
private val messageReceivingThreadContext = newSingleThreadContext("AwalaManagerMessageReceiverThread")

private val _incomingMessages = Channel<AwalaIncomingMessage<*>>()
override val incomingMessages: Flow<AwalaIncomingMessage<*>>
get() = _incomingMessages.receiveAsFlow()

private var isAwalaSetUp = AtomicBoolean(false)
private var awalaSetupJob: Job? = null

Expand All @@ -72,7 +69,6 @@ class AwalaManagerImpl @Inject constructor(
private var isReceivingMessages = false

private var firstPartyEndpoint: FirstPartyEndpoint? = null

private var thirdPartyServerEndpoint: ThirdPartyEndpoint? = null

init {
Expand Down Expand Up @@ -125,6 +121,27 @@ class AwalaManagerImpl @Inject constructor(
return isInstalled
}

override suspend fun authorizeUsers(thirdPartyPublicKey: ByteArray) {
withContext(awalaThreadContext) {
val firstPartyEndpoint = loadFirstPartyEndpoint()
val auth = firstPartyEndpoint.authorizeIndefinitely(thirdPartyPublicKey)
sendMessage(
outgoingMessage = AwalaOutgoingMessage(
type = MessageType.ContactPairingAuthorization,
content = auth,
),
recipient = MessageRecipient.Server(),
)
}
}

override suspend fun getFirstPartyPublicKey(): String {
return withContext(awalaThreadContext) {
val firstPartyEndpoint = loadFirstPartyEndpoint()
Base64.encodeToString(firstPartyEndpoint.publicKey.encoded, Base64.NO_WRAP)
}
}

private suspend fun loadFirstPartyEndpoint(): FirstPartyEndpoint {
return withContext(awalaThreadContext) {
val firstPartyEndpointNodeId = awalaRepository.getServerFirstPartyEndpointNodeId()
Expand Down Expand Up @@ -167,10 +184,9 @@ class AwalaManagerImpl @Inject constructor(

Log.i(TAG, "start receiving messages...")
GatewayClient.receiveMessages().collect { message ->
val type = MessageType.from(message.type)
val parsedMessage = parser.parse(type, message.content)
.also { Log.i(TAG, "Receive message: ($it)") }
_incomingMessages.send(parsedMessage)
Log.i(TAG, "Receive message: ${message.type}: ($message)")
processor.process(message, this@AwalaManagerImpl)
Log.i(TAG, "Message processed")
message.ack()
}
}
Expand Down Expand Up @@ -262,8 +278,8 @@ class AwalaManagerImpl @Inject constructor(
}
}

private companion object {
private const val TAG = "AwalaManager"
companion object {
const val TAG = "AwalaManager"
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
package tech.relaycorp.letro.awala.message

import android.util.Log
import tech.relaycorp.letro.awala.AwalaManagerImpl

sealed class MessageType(val value: String) {
object AccountCreationRequest : MessageType("application/vnd.relaycorp.letro.account-creation-request")
object AccountCreationCompleted : MessageType("application/vnd.relaycorp.letro.account-creation-completed-tmp")
Expand All @@ -18,7 +21,10 @@ sealed class MessageType(val value: String) {
ContactPairingRequest.value -> ContactPairingRequest
ContactPairingMatch.value -> ContactPairingMatch
ContactPairingAuthorization.value -> ContactPairingAuthorization
else -> Unknown
else -> {
Log.e(AwalaManagerImpl.TAG, "Unknown message type $type")
Unknown
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,7 @@
package tech.relaycorp.letro.awala.parser

import tech.relaycorp.letro.awala.message.AwalaIncomingMessage
import tech.relaycorp.letro.awala.message.MessageType

interface AwalaMessageParser {
fun parse(type: MessageType, content: ByteArray): AwalaIncomingMessage<*>
}

class AwalaMessageParserImpl constructor(
private val parsers: Map<MessageType, AwalaMessageParser>,
) : AwalaMessageParser {
override fun parse(type: MessageType, content: ByteArray): AwalaIncomingMessage<*> {
return parsers[type]?.parse(type, content) ?: throw IllegalStateException("No parser for messageType: $type")
}
fun parse(content: ByteArray): AwalaIncomingMessage<*>
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package tech.relaycorp.letro.awala.processor

import tech.relaycorp.awaladroid.messaging.IncomingMessage
import tech.relaycorp.letro.awala.AwalaManager
import tech.relaycorp.letro.awala.message.MessageType

interface AwalaMessageProcessor {
suspend fun process(message: IncomingMessage, awalaManager: AwalaManager)
}

class AwalaMessageProcessorImpl constructor(
private val processors: Map<MessageType, AwalaMessageProcessor>,
) : AwalaMessageProcessor {

override suspend fun process(message: IncomingMessage, awalaManager: AwalaManager) {
val type = MessageType.from(message.type)
processors[type]!!.process(message, awalaManager)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package tech.relaycorp.letro.awala.processor

import android.util.Log
import tech.relaycorp.awaladroid.messaging.IncomingMessage
import tech.relaycorp.letro.awala.AwalaManager
import tech.relaycorp.letro.awala.AwalaManagerImpl
import javax.inject.Inject

class UnknownMessageProcessor @Inject constructor() : AwalaMessageProcessor {

override suspend fun process(message: IncomingMessage, awalaManager: AwalaManager) {
Log.w(AwalaManagerImpl.TAG, "Unknown message processor for type: ${message.type}")
}
}
25 changes: 17 additions & 8 deletions app/src/main/java/tech/relaycorp/letro/contacts/model/Contact.kt
Original file line number Diff line number Diff line change
@@ -1,10 +1,16 @@
package tech.relaycorp.letro.contacts.model

import androidx.annotation.IntDef
import androidx.room.Entity
import androidx.room.ForeignKey
import androidx.room.Index
import androidx.room.PrimaryKey
import tech.relaycorp.letro.account.model.Account
import tech.relaycorp.letro.contacts.model.ContactPairingStatus.Companion.AUTHORIZATION_SENT
import tech.relaycorp.letro.contacts.model.ContactPairingStatus.Companion.COMPLETED
import tech.relaycorp.letro.contacts.model.ContactPairingStatus.Companion.MATCH
import tech.relaycorp.letro.contacts.model.ContactPairingStatus.Companion.REQUEST_SENT
import tech.relaycorp.letro.contacts.model.ContactPairingStatus.Companion.UNPAIRED

const val TABLE_NAME_CONTACTS = "contacts"

Expand All @@ -22,18 +28,21 @@ const val TABLE_NAME_CONTACTS = "contacts"
)
data class Contact(
@PrimaryKey(autoGenerate = true)
val id: Long,
val id: Long = 0L,
val ownerVeraId: String,
val contactVeraId: String,
val alias: String? = null,
val contactEndpointId: String? = null,
val status: ContactPairingStatus = ContactPairingStatus.Unpaired,
@ContactPairingStatus val status: Int = UNPAIRED,
)

sealed interface ContactPairingStatus {
object Unpaired : ContactPairingStatus
object RequestSent : ContactPairingStatus
object Match : ContactPairingStatus
object AuthorizationSent : ContactPairingStatus
object Complete : ContactPairingStatus
@IntDef(UNPAIRED, REQUEST_SENT, MATCH, AUTHORIZATION_SENT, COMPLETED)
annotation class ContactPairingStatus {
companion object {
const val UNPAIRED = 0
const val REQUEST_SENT = 1
const val MATCH = 2
const val AUTHORIZATION_SENT = 3
const val COMPLETED = 4
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,12 @@ interface ContactsDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insert(contact: Contact): Long

@Query("SELECT * FROM $TABLE_NAME_CONTACTS WHERE ownerVeraId = :ownerVeraId AND contactVeraId = :contactVeraId")
suspend fun getContact(
ownerVeraId: String,
contactVeraId: String,
): Contact?

@Update
suspend fun update(contact: Contact)

Expand Down
Loading