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

Federation Data Exchange for FE2 #1943

Merged
merged 18 commits into from
Jun 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ import com.github.dedis.popstellar.model.network.method.message.data.election.El
import com.github.dedis.popstellar.model.network.method.message.data.election.ElectionResult
import com.github.dedis.popstellar.model.network.method.message.data.election.ElectionSetup
import com.github.dedis.popstellar.model.network.method.message.data.federation.Challenge
import com.github.dedis.popstellar.model.network.method.message.data.federation.FederationResult
import com.github.dedis.popstellar.model.network.method.message.data.federation.TokensExchange
import com.github.dedis.popstellar.model.network.method.message.data.lao.CreateLao
import com.github.dedis.popstellar.model.network.method.message.data.lao.GreetLao
import com.github.dedis.popstellar.model.network.method.message.data.lao.StateLao
Expand Down Expand Up @@ -259,6 +261,18 @@ object DataRegistryModule {
linkedOrganizationsHandler.handleChallenge(context, challenge)
}

builder.add(Objects.FEDERATION, Action.RESULT, FederationResult::class.java) {
context: HandlerContext,
result: FederationResult ->
linkedOrganizationsHandler.handleResult(context, result)
}

builder.add(Objects.FEDERATION, Action.TOKENS_EXCHANGE, TokensExchange::class.java) {
context: HandlerContext,
tokenExchange: TokensExchange ->
linkedOrganizationsHandler.handleTokensExchange(context, tokenExchange)
}

return builder.build()
}

Expand Down Expand Up @@ -327,6 +341,8 @@ object DataRegistryModule {

// Federation
builder.add(Objects.FEDERATION, Action.CHALLENGE, Challenge::class.java, null)
builder.add(Objects.FEDERATION, Action.RESULT, FederationResult::class.java, null)
builder.add(Objects.FEDERATION, Action.TOKENS_EXCHANGE, TokensExchange::class.java, null)

return builder.build()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ enum class Action
CHALLENGE("challenge"),
INIT("init"),
EXPECT("expect"),
TOKENS_EXCHANGE("tokens_exchange"),
RUMOR("rumor");

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package com.github.dedis.popstellar.model.network.method.message.data.federation

import com.github.dedis.popstellar.model.network.method.message.MessageGeneral
import com.github.dedis.popstellar.model.network.method.message.data.Action
import com.github.dedis.popstellar.model.network.method.message.data.Data
import com.github.dedis.popstellar.model.network.method.message.data.Objects
import com.github.dedis.popstellar.utility.MessageValidator
import com.google.gson.annotations.SerializedName

/** Informs about the result of the authentication procedure */
class FederationResult
/**
* Constructor for a data Federation Result
*
* @param status status of the result (either success or failure)
* @param reason reason of the failure
* @param publicKey public key of the other LAO organizer
* @param challenge challenge used to connect the LAOs
*/
(
val status: String,
val reason: String? = null,
@SerializedName("public_key") val publicKey: String? = null,
val challenge: MessageGeneral,
) : Data {

init {
MessageValidator.verify().isValidFederationResult(status, reason, publicKey, challenge)
}

override val `object`: String
get() = Objects.FEDERATION.`object`

override val action: String
get() = Action.RESULT.action

override fun equals(other: Any?): Boolean {
if (this === other) {
return true
}
if (other == null || javaClass != other.javaClass) {
return false
}
val that = other as FederationResult
return status == that.status &&
reason == that.reason &&
publicKey == that.publicKey &&
challenge == that.challenge
}

override fun hashCode(): Int {
return java.util.Objects.hash(status, reason, publicKey, challenge)
}

override fun toString(): String {
if (status == FAILURE) {
return "FederationResult{status='$status', reason='$reason', challenge='$challenge'}"
} else if (status == SUCCESS) {
return "FederationResult{status='$status', public_key='$publicKey', " +
"challenge='$challenge'}"
}
return "FederationResult{ERROR}"
}

fun isSuccess(): Boolean {
return status == SUCCESS
}

companion object {
const val SUCCESS = "success"
const val FAILURE = "failure"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package com.github.dedis.popstellar.model.network.method.message.data.federation

import com.github.dedis.popstellar.model.network.method.message.data.Action
import com.github.dedis.popstellar.model.network.method.message.data.Data
import com.github.dedis.popstellar.model.network.method.message.data.Objects
import com.github.dedis.popstellar.utility.MessageValidator
import com.google.gson.annotations.SerializedName

/** Token exchange to be broadcast in the LAO */
class TokensExchange
/**
* Constructor for a data TokenExchange
*
* @param laoId ID of the remote LAO
* @param rollCallId ID of the rollCall of the remote LAO
* @param tokens array of tokens contained in the rollCall
* @param timestamp timestamp of the message
*/
(
@SerializedName("lao_id") val laoId: String,
@SerializedName("roll_call_id") val rollCallId: String,
val tokens: Array<String>,
val timestamp: Long
) : Data {

init {
MessageValidator.verify()
.isNotEmptyBase64(laoId, "lao_id")
.isBase64(rollCallId, "roll_call_id")
.arrayElementsNotEmptyBase64(tokens, field = "tokens")
.validPastTimes(timestamp)
}

override val `object`: String
get() = Objects.FEDERATION.`object`

override val action: String
get() = Action.TOKENS_EXCHANGE.action

override fun equals(other: Any?): Boolean {
if (this === other) {
return true
}
if (other == null || javaClass != other.javaClass) {
return false
}
val that = other as TokensExchange
return laoId == that.laoId &&
rollCallId == that.rollCallId &&
tokens.contentEquals(that.tokens) &&
timestamp == that.timestamp
}

override fun hashCode(): Int {
return java.util.Objects.hash(laoId, rollCallId, tokens, timestamp)
}

override fun toString(): String {
return "TokensExchange{lao_id='$laoId', roll_call_id='$rollCallId', " +
"tokens='$tokens', timestamp='$timestamp'}"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,16 @@ class Chirp : Copyable<Chirp> {
val timestamp: Long
val isDeleted: Boolean
val parentId: MessageID
val laoId: String

constructor(
id: MessageID,
sender: PublicKey,
text: String,
timestamp: Long,
isDeleted: Boolean,
parentId: MessageID
parentId: MessageID,
laoId: String
) {
require(id.encoded.isNotEmpty()) { "The id of the Chirp is empty" }
require(timestamp >= 0) { "The timestamp of the Chirp is negative" }
Expand All @@ -32,15 +34,17 @@ class Chirp : Copyable<Chirp> {
this.timestamp = timestamp
this.parentId = parentId
this.isDeleted = isDeleted
this.laoId = laoId
}

constructor(
id: MessageID,
sender: PublicKey,
text: String,
timestamp: Long,
parentId: MessageID
) : this(id, sender, text, timestamp, false, parentId)
parentId: MessageID,
laoId: String
) : this(id, sender, text, timestamp, false, parentId, laoId)

constructor(chirp: Chirp, deleted: Boolean) {
id = chirp.id
Expand All @@ -49,6 +53,7 @@ class Chirp : Copyable<Chirp> {
timestamp = chirp.timestamp
parentId = chirp.parentId
isDeleted = deleted
laoId = chirp.laoId
}

constructor(chirp: Chirp) {
Expand All @@ -58,6 +63,7 @@ class Chirp : Copyable<Chirp> {
timestamp = chirp.timestamp
isDeleted = chirp.isDeleted
parentId = chirp.parentId
laoId = chirp.laoId
}

override fun copy(): Chirp {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,14 @@
package com.github.dedis.popstellar.repository

import android.app.Activity
import android.app.Application
import androidx.lifecycle.Lifecycle
import com.github.dedis.popstellar.model.network.method.message.data.federation.Challenge
import com.github.dedis.popstellar.utility.GeneralUtils
import io.reactivex.disposables.CompositeDisposable
import io.reactivex.disposables.Disposable
import java.util.EnumMap
import java.util.function.Consumer
import javax.inject.Inject
import javax.inject.Singleton

Expand All @@ -10,18 +18,25 @@ import javax.inject.Singleton
* Its main purpose is to store received messages
*/
@Singleton
class LinkedOrganizationsRepository @Inject constructor() {
class LinkedOrganizationsRepository @Inject constructor(application: Application) {
private var challenge: Challenge? = null
private var onChallengeUpdatedCallback: ((Challenge) -> Unit)? = null
private var linkedLaos: MutableMap<String, MutableMap<String, Array<String>>> = mutableMapOf()
private var onLinkedLaosUpdatedCallback: ((String, MutableMap<String, Array<String>>) -> Unit)? =
null
private var newTokensNotifyFunction: ((String, String, String, Array<String>) -> Unit)? = null
private val disposables = CompositeDisposable()
var otherLaoId: String? = null
var otherServerAddr: String? = null
var otherPublicKey: String? = null

/**
* Updates the challenge
*
* @param challenge the new Challenge
*/
init {
val consumerMap: MutableMap<Lifecycle.Event, Consumer<Activity>> =
EnumMap(Lifecycle.Event::class.java)
consumerMap[Lifecycle.Event.ON_STOP] = Consumer { disposables.clear() }
application.registerActivityLifecycleCallbacks(GeneralUtils.buildLifecycleCallback(consumerMap))
}

fun updateChallenge(challenge: Challenge) {
this.challenge = challenge
onChallengeUpdatedCallback?.invoke(challenge)
Expand All @@ -35,6 +50,40 @@ class LinkedOrganizationsRepository @Inject constructor() {
return challenge
}

fun addLinkedLao(laoId: String, otherLaoId: String, tokens: Array<String>) {
val laoMap = linkedLaos.getOrPut(laoId) { mutableMapOf() }
laoMap[otherLaoId] = tokens
onLinkedLaosUpdatedCallback?.invoke(laoId, laoMap)
}

fun updateAndNotifyLinkedLao(
laoId: String,
otherLaoId: String,
tokens: Array<String>,
rollCallId: String
) {
addLinkedLao(laoId, otherLaoId, tokens)
newTokensNotifyFunction?.invoke(laoId, otherLaoId, rollCallId, tokens)
Comment on lines +65 to +66
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you take a look at other repos (any one, e.g. rollcallrepo) you'd see that in general this is not the preferred way to notify modifications on a given data structure, but that we use java rx constructs (subject, observables and so on)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well the thing is that I tried using observables and for some reason I had many problems with the UI not updating or weird other issues... That's why in the end I changed to using callbacks, I know it's a bit more complex but at least it's working

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, we can keep it as is then

}

fun setOnLinkedLaosUpdatedCallback(
callback: (String, MutableMap<String, Array<String>>) -> Unit
) {
onLinkedLaosUpdatedCallback = callback
}

fun setNewTokensNotifyFunction(function: (String, String, String, Array<String>) -> Unit) {
newTokensNotifyFunction = function
}

fun getLinkedLaos(laoId: String): MutableMap<String, Array<String>> {
return linkedLaos.getOrDefault(laoId, mutableMapOf())
}

fun addDisposable(disposable: Disposable) {
disposables.add(disposable)
}

fun flush() {
otherLaoId = null
otherServerAddr = null
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ import com.github.dedis.popstellar.ui.lao.event.LaoDetailAnimation.showIn
import com.github.dedis.popstellar.ui.lao.event.LaoDetailAnimation.showOut
import com.github.dedis.popstellar.ui.qrcode.QrScannerFragment
import com.github.dedis.popstellar.ui.qrcode.ScanningAction
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch

/**
* A simple [Fragment] subclass. Use the [LinkedOrganizationsFragment.newInstance] factory method to
Expand All @@ -41,9 +44,6 @@ class LinkedOrganizationsFragment : Fragment() {
linkedOrganizationsViewModel =
obtainLinkedOrganizationsViewModel(requireActivity(), laoViewModel.laoId)

// Starts from a clean repository
linkedOrganizationsViewModel.flushRepository()

// Sets the text and the button depending on the user's role
laoViewModel.role.observe(viewLifecycleOwner) { role: Role ->
if (role == Role.ORGANIZER) {
Expand All @@ -59,6 +59,19 @@ class LinkedOrganizationsFragment : Fragment() {
binding.inviteOtherOrganization.setOnClickListener(invitationPage)
binding.joinOtherOrganizationInvitation.setOnClickListener(joinButton)

// Displaying the linked organizations
val laos = linkedOrganizationsViewModel.getLinkedLaosMap().keys
displayLinkedOrganizations(laos)
linkedOrganizationsViewModel.doWhenLinkedLaosIsUpdated { laoId, laoMap ->
if (laoId == laoViewModel.laoId) {
CoroutineScope(Dispatchers.Main).launch {
val currentLaos = laoMap.keys
displayLinkedOrganizations(currentLaos)
}
}
}

linkedOrganizationsViewModel.setLinkedLaosNotifyFunction()
Comment on lines +62 to +74
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why calling display first with the current list and then putting a custom and complex observer that uses coroutines and not directly using a simple observer? This relates to my previous comment on how to deal with modifications using observables and subjects

handleBackNav()

return binding.root
Expand Down Expand Up @@ -97,13 +110,27 @@ class LinkedOrganizationsFragment : Fragment() {

private var joinButton =
View.OnClickListener {
linkedOrganizationsViewModel.flushRepository()
laoViewModel.setIsTab(false)
linkedOrganizationsViewModel.manager = parentFragmentManager
LaoActivity.setCurrentFragment(parentFragmentManager, R.id.fragment_qr_scanner) {
QrScannerFragment.newInstance(ScanningAction.FEDERATION_JOIN)
}
}

private fun displayLinkedOrganizations(laos: Set<String>) {
if (laos.isNotEmpty()) {
val laosText = laos.joinToString(separator = "\n\n")
val textToDisplay = context?.getString(R.string.list_organizations, laosText)
binding.noOrganizationsText.visibility = View.GONE
binding.listOrganizationsText.visibility = View.VISIBLE
binding.listOrganizationsText.text = textToDisplay
} else {
binding.listOrganizationsText.visibility = View.GONE
binding.noOrganizationsText.visibility = View.VISIBLE
}
}

private fun handleBackNav() {
LaoActivity.addBackNavigationCallbackToEvents(requireActivity(), viewLifecycleOwner, TAG)
}
Expand Down
Loading
Loading