-
-
Notifications
You must be signed in to change notification settings - Fork 2
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
SpeziAccount & SpeziFirebase #107
base: main
Are you sure you want to change the base?
Changes from 11 commits
be77221
291503e
dec212c
a9db0b5
58de069
1ea20b0
5ebc29a
d740dcf
10bd195
bfd7e97
341a9dd
7339be9
579f4ce
8ae6519
ce2c314
38c0ce6
f25889f
6d7b4f2
18bdfbc
7b6c87c
aaf29df
11efb05
5fa1c8d
37159b8
f3f516b
306b46b
dc8133b
4258420
e3baa1b
0a94bc5
40601b4
728e534
7928eb2
d974d16
5143693
2c8e57b
10a2543
87f6f18
11aa4f3
47b8841
22c3e6d
bc50be6
3db9aed
5c2d056
6f5b66c
e02bad3
0a0fc2f
35f5076
307d84c
fcda9c7
088e66f
2207da9
9962c09
1783d43
c84e748
d184b64
58dc269
858c75f
f3af7e3
ace70d4
ca54fef
ace5c51
df7d2b1
e4da71a
6566ef5
004be47
04ab47a
286f416
c6a072d
132f692
5f3651f
b535893
de0fb1a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,39 @@ | ||
package edu.stanford.spezi.module.account | ||
|
||
import androidx.annotation.MainThread | ||
import androidx.compose.material3.Text | ||
import androidx.compose.material3.TextField | ||
import androidx.compose.runtime.Composable | ||
import androidx.compose.runtime.MutableState | ||
import kotlin.reflect.KClass | ||
import kotlin.reflect.KProperty1 | ||
|
||
// SpeziAccount | ||
|
||
/* | ||
|
||
interface AccountKeyInput<Value : Any> { | ||
val id: String? | ||
val name: String | ||
val category: AccountKeyCategory | ||
val displayView: @Composable (Value) -> Unit | ||
val entryView: @Composable (MutableState<Value>) -> Unit | ||
} | ||
|
||
annotation class AccountValue<Input: AccountKeyInput<*>> | ||
|
||
data class NameAccountKeyInput(val input: Unit): AccountKeyInput<String> { | ||
override val id: String? get() = null | ||
override val name: String get() = "Name" | ||
override val category: AccountKeyCategory get() = AccountKeyCategory.name | ||
override val displayView: @Composable (String) -> Unit get() = { text -> | ||
Text(text) | ||
} | ||
override val entryView: @Composable (MutableState<String>) -> Unit get() = { state -> | ||
TextField(value = state.value, onValueChange = { state.value = it }) | ||
} | ||
} | ||
|
||
@AccountValue<NameAccountKeyInput> | ||
val AccountDetails.name: String? get() = null | ||
*/ |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
package edu.stanford.spezi.module.account.account | ||
|
||
import edu.stanford.spezi.module.account.account.service.AccountService | ||
import edu.stanford.spezi.module.account.account.value.collections.AccountDetails | ||
|
||
class Account( | ||
service: AccountService, | ||
configuration: AccountValueConfiguration = AccountValueConfiguration.default, | ||
details: AccountDetails? = null | ||
) { | ||
var signedIn: Boolean = details != null | ||
private set | ||
|
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
package edu.stanford.spezi.module.account.account | ||
|
||
import edu.stanford.spezi.core.logging.speziLogger | ||
import edu.stanford.spezi.module.account.account.service.AccountService | ||
import edu.stanford.spezi.module.account.spezi.Module | ||
import edu.stanford.spezi.module.account.spezi.Standard | ||
|
||
class AccountConfiguration<Service: AccountService>: Module { | ||
private val logger by speziLogger() | ||
|
||
val account: Account = TODO() | ||
private val externalStorage: ExternalAccountStorage = TODO() | ||
private val accountService: Service = TODO() | ||
private val storageProvider: List<Module> = TODO() | ||
private val standard: Standard = TODO() | ||
|
||
override fun configure() { | ||
TODO() | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,49 @@ | ||
package edu.stanford.spezi.module.account.account | ||
|
||
import edu.stanford.spezi.core.logging.speziLogger | ||
import edu.stanford.spezi.module.account.account.value.collections.AccountDetails | ||
import edu.stanford.spezi.module.account.account.value.collections.AccountKey | ||
import edu.stanford.spezi.module.account.account.value.collections.AccountModifications | ||
import edu.stanford.spezi.module.account.spezi.Module | ||
import kotlin.reflect.KClass | ||
|
||
class AccountDetailsCache: Module { | ||
private val logger by speziLogger() | ||
|
||
private val localCache = mutableMapOf<String, AccountDetails>() | ||
|
||
fun loadEntry(accountId: String, keys: Set<KClass<AccountKey<*>>>): AccountDetails? { | ||
localCache[accountId]?.let { | ||
return it | ||
} | ||
|
||
return null // TODO: lead from persistency as well | ||
} | ||
|
||
fun clearEntry(accountId: String) { | ||
localCache.remove(accountId) | ||
// TODO: Delete persistence as well | ||
} | ||
|
||
internal fun purgeMemoryCache(accountId: String) { | ||
localCache.remove(accountId) | ||
} | ||
|
||
fun communicateModifications(accountId: String, modifications: AccountModifications) { | ||
val details = AccountDetails() | ||
localCache[accountId]?.let { | ||
// TODO("AccountDetails.add(contentsOf:) missing!") | ||
} | ||
// TODO("AccountDetails.add(contentsOf:merge:) missing!") | ||
// TODO("AccountDetails.removeAll() missing!") | ||
|
||
communicateRemoteChanges(accountId, details) | ||
} | ||
|
||
fun communicateRemoteChanges(accountId: String, details: AccountDetails) { | ||
localCache[accountId] = details | ||
|
||
// TODO: Persistent store missing | ||
} | ||
|
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,61 @@ | ||
package edu.stanford.spezi.module.account.account | ||
|
||
import android.text.style.TabStopSpan.Standard | ||
import edu.stanford.spezi.module.account.account.value.collections.AccountDetails | ||
import edu.stanford.spezi.module.account.account.value.keys.accountId | ||
import kotlinx.coroutines.flow.Flow | ||
import kotlinx.coroutines.flow.FlowCollector | ||
import kotlinx.coroutines.flow.flow | ||
import kotlinx.coroutines.flow.onCompletion | ||
import kotlinx.coroutines.sync.Mutex | ||
import kotlinx.coroutines.sync.withLock | ||
import java.util.UUID | ||
|
||
class AccountNotifications { | ||
sealed interface Event { | ||
data class DeletingAccount(val accountId: String): Event | ||
data class AssociatedAccount(val details: AccountDetails): Event | ||
data class DetailsChanged(val previous: AccountDetails, val new: AccountDetails): Event | ||
data class DisassociatingAccount(val details: AccountDetails): Event | ||
} | ||
|
||
private val standard: Standard = TODO() | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. we will define our own type right? as this is coming from There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, this is just a placeholder for now |
||
private val storage: ExternalAccountStorage = TODO() | ||
private val collectors = mutableMapOf<UUID, FlowCollector<Event>>() | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. FYI: we can use a concurrent hash map and can avoid the need for the mutex here private val collectors = ConcurrentHashMap<UUID, FlowCollector<Event>>() |
||
private val mutex = Mutex() | ||
|
||
val events: Flow<Event> get() = newSubscription() | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why do we need to keep a reference of the collectors in the map above? Currently we are manually setting entries there in TLDR: I know this ways done to match the internal implementation in iOS, but the equivalent in android can be achieved as follows:
This approach is also safer in regard to wrong usages from clients, because they might access the events property multiple times from their components, which would result into creating new instances every time - while with the proposal we are returning the same instance (single source of truth) on every access The only difference is that the shared flow is hot - technical meaning is that it will emit always even though it might not have any active subscribers / collectors. However, flow api is pretty efficient and we can really ignore the fact the we might emit even though no one is listening |
||
|
||
suspend fun respondToEvent(event: Event) { | ||
(standard as? AccountNotifyConstraint)?.respondToEvent(event) | ||
|
||
when (event) { | ||
is Event.DeletingAccount -> { | ||
storage.willDeleteAccount(event.accountId) | ||
} | ||
is Event.DisassociatingAccount -> { | ||
storage.userWillDisassociate(event.details.accountId) | ||
} | ||
else -> {} | ||
} | ||
|
||
mutex.withLock { | ||
for (collector in collectors.values) { | ||
collector.emit(event) | ||
} | ||
} | ||
} | ||
|
||
private fun newSubscription(): Flow<Event> { | ||
val key = UUID.randomUUID() | ||
return flow { | ||
mutex.withLock { | ||
collectors[key] = this | ||
} | ||
}.onCompletion { | ||
mutex.withLock { | ||
collectors.remove(key) | ||
} | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
package edu.stanford.spezi.module.account.account | ||
|
||
interface AccountNotifyConstraint { | ||
fun respondToEvent(event: AccountNotifications.Event) | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
package edu.stanford.spezi.module.account.account | ||
|
||
import edu.stanford.spezi.module.account.account.value.collections.AccountDetails | ||
import edu.stanford.spezi.module.account.account.value.collections.AccountKey | ||
import edu.stanford.spezi.module.account.account.value.collections.AccountModifications | ||
import edu.stanford.spezi.module.account.spezi.Module | ||
import kotlin.reflect.KClass | ||
|
||
interface AccountStorageProvider: Module { | ||
suspend fun load(accountId: String, keys: Set<KClass<AccountKey<*>>>): AccountDetails? | ||
suspend fun store(accountId: String, details: AccountDetails) { | ||
val modifications = AccountModifications(modifiedDetails = details) | ||
store(accountId, modifications) | ||
} | ||
suspend fun store(accountId: String, modifications: AccountModifications) | ||
suspend fun disassociate(accountId: String) | ||
suspend fun delete(accountId: String) | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,74 @@ | ||
package edu.stanford.spezi.module.account.account | ||
|
||
import edu.stanford.spezi.module.account.account.value.collections.AccountDetails | ||
import edu.stanford.spezi.module.account.account.value.collections.AccountKey | ||
import edu.stanford.spezi.module.account.account.value.collections.AccountModifications | ||
import edu.stanford.spezi.module.account.account.value.keys.isIncomplete | ||
import edu.stanford.spezi.module.account.spezi.Module | ||
import kotlinx.coroutines.flow.Flow | ||
import kotlinx.coroutines.flow.FlowCollector | ||
import kotlinx.coroutines.flow.flow | ||
import kotlinx.coroutines.flow.onCompletion | ||
import java.util.UUID | ||
import kotlin.reflect.KClass | ||
|
||
class ExternalAccountStorage: Module { | ||
|
||
data class ExternallyStoredDetails( | ||
val accountId: String, | ||
val details: AccountDetails, | ||
) | ||
|
||
private var subscriptions = mutableMapOf<UUID, FlowCollector<ExternallyStoredDetails>>() | ||
private var storageProvider: AccountStorageProvider? = null | ||
|
||
val updatedDetails: Flow<ExternallyStoredDetails> get() { | ||
val id = UUID.randomUUID() | ||
return flow { | ||
subscriptions[id] = this | ||
}.onCompletion { | ||
subscriptions.remove(id) | ||
} | ||
} | ||
|
||
suspend fun notifyAboutUpdatedDetails(accountId: String, details: AccountDetails) { | ||
val newDetails = details.copy() | ||
newDetails.isIncomplete = false | ||
val update = ExternallyStoredDetails(accountId, newDetails) | ||
for (subscription in subscriptions) { | ||
subscription.value.emit(update) | ||
} | ||
} | ||
|
||
suspend fun requestExternalStorage(accountId: String, details: AccountDetails) { | ||
// TODO: Check for emptiness and making sure storageProvider exists | ||
storageProvider?.store(accountId, details) | ||
} | ||
|
||
suspend fun retrieveExternalStorage(accountId: String, keys: List<KClass<AccountKey<*>>>): AccountDetails { | ||
if (keys.isEmpty()) return AccountDetails() | ||
|
||
storageProvider?.let { storageProvider -> | ||
storageProvider.load(accountId, keys)?.let { details -> | ||
return details | ||
} | ||
val details = AccountDetails() | ||
details.isIncomplete = true | ||
return details | ||
} ?: throw Error("") | ||
} | ||
|
||
suspend fun updateExternalStorage(accountId: String, modifications: AccountModifications) { | ||
val storageProvider = storageProvider ?: throw Error("") | ||
storageProvider.store(accountId, modifications) | ||
} | ||
|
||
suspend fun willDeleteAccount(accountId: String) { | ||
storageProvider?.delete(accountId) | ||
} | ||
|
||
suspend fun userWillDisassociate(accountId: String) { | ||
storageProvider?.disassociate(accountId) | ||
} | ||
|
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
package edu.stanford.spezi.module.account.account.compositionLocal | ||
|
||
import androidx.compose.runtime.compositionLocalOf | ||
|
||
enum class FollowUpBehavior { | ||
DISABLED, MINIMAL, REDUNDANT; | ||
|
||
companion object { | ||
val automatic: FollowUpBehavior get() = MINIMAL | ||
} | ||
} | ||
|
||
val localFollowUpBehaviorAfterSetup = compositionLocalOf { FollowUpBehavior.automatic } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It would be great to align on the following pattern: Every public component will be defined as an interface, e.g.
interface AccountNotifications
, and the actual implementation as aninternal class AccountNotificationsImpl : AccountNotifications
. This approach will give the consumers of the library to use test doubles of their choice for testing, e.g. mocking or fakes.Also, I would rethink the public API of this component, it should indeed not offer the respond to event method publicly as it allows mutation from outside, while we only want to mutate internally in
Account
class