[#764] Multiplatform remote config API

This commit is contained in:
Carter Jernigan 2023-02-19 09:03:30 -05:00 committed by Carter Jernigan
parent b3f4601b14
commit 6b202e3c9f
38 changed files with 587 additions and 51 deletions

View File

@ -0,0 +1,33 @@
plugins {
kotlin("multiplatform")
id("secant.kotlin-multiplatform-build-conventions")
id("secant.dependency-conventions")
}
kotlin {
jvm()
sourceSets {
getByName("commonMain") {
dependencies {
api(libs.kotlinx.datetime)
api(libs.kotlinx.coroutines.core)
api(libs.kotlinx.immutable)
}
}
getByName("commonTest") {
dependencies {
implementation(kotlin("test"))
api(libs.kotlinx.coroutines.test)
}
}
getByName("jvmMain") {
dependencies {
}
}
getByName("jvmTest") {
dependencies {
implementation(kotlin("test"))
}
}
}
}

View File

@ -0,0 +1,25 @@
package co.electriccoin.zcash.configuration.api
import co.electriccoin.zcash.configuration.model.map.Configuration
import kotlinx.coroutines.flow.Flow
/**
* Provides a remote config implementation.
*/
interface ConfigurationProvider {
/**
* @return The configuration if it has been loaded already. If not loaded, returns an empty configuration.
*/
fun peekConfiguration(): Configuration
/**
* @return A flow that provides snapshots of configuration updates.
*/
fun getConfigurationFlow(): Flow<Configuration>
/**
* Signals to the configuration provider that now might be a good time to refresh.
*/
fun hintToRefresh()
}

View File

@ -0,0 +1,53 @@
package co.electriccoin.zcash.configuration.api
import co.electriccoin.zcash.configuration.model.entry.ConfigKey
import co.electriccoin.zcash.configuration.model.map.Configuration
import kotlinx.collections.immutable.PersistentList
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine
import kotlinx.datetime.Instant
class MergingConfigurationProvider(private val configurationProviders: PersistentList<ConfigurationProvider>) : ConfigurationProvider {
override fun peekConfiguration(): Configuration {
return MergingConfiguration(configurationProviders.map { it.peekConfiguration() }.toPersistentList())
}
override fun getConfigurationFlow(): Flow<Configuration> {
return combine(configurationProviders.map { it.getConfigurationFlow() }) { configurations ->
MergingConfiguration(configurations.toList())
}
}
override fun hintToRefresh() {
configurationProviders.forEach { it.hintToRefresh() }
}
}
private data class MergingConfiguration(private val configurations: List<Configuration>) : Configuration {
override val updatedAt: Instant?
get() = configurations.mapNotNull { it.updatedAt }.maxOrNull()
override fun hasKey(key: ConfigKey): Boolean {
return null != configurations.firstWithKey(key)
}
override fun getBoolean(key: ConfigKey, defaultValue: Boolean): Boolean {
return configurations.firstWithKey(key)?.let {
return it.getBoolean(key, defaultValue)
} ?: defaultValue
}
override fun getInt(key: ConfigKey, defaultValue: Int): Int {
return configurations.firstWithKey(key)?.let {
return it.getInt(key, defaultValue)
} ?: defaultValue
}
override fun getString(key: ConfigKey, defaultValue: String): String {
return configurations.firstWithKey(key)?.let {
return it.getString(key, defaultValue)
} ?: defaultValue
}
}
private fun List<Configuration>.firstWithKey(key: ConfigKey): Configuration? = firstOrNull { it.hasKey(key) }

View File

@ -0,0 +1,12 @@
package co.electriccoin.zcash.configuration.model.entry
import co.electriccoin.zcash.configuration.model.map.Configuration
data class BooleanConfigurationEntry(
override val key: ConfigKey,
private val defaultValue: Boolean
) : DefaultEntry<Boolean> {
override fun getValue(configuration: Configuration) =
configuration.getBoolean(key, defaultValue)
}

View File

@ -0,0 +1,37 @@
package co.electriccoin.zcash.configuration.model.entry
/**
* Defines a configuration key.
*
* Different configuration providers have unique restrictions on keys. This attempts to find a
* least common denominator with some reasonable limits on what the keys can contain.
*/
@JvmInline
value class ConfigKey(val key: String) {
init {
requireKeyConstraints(key)
}
companion object {
private const val MIN_KEY_LENGTH = 1
private const val MAX_KEY_LENGTH = 256
private val REGEX = Regex("[a-zA-Z0-9_]*")
/**
* Checks a configuration key against known constraints.
*
* @param key Key to check.
*/
private fun requireKeyConstraints(key: String) {
require(key.length in 1..MAX_KEY_LENGTH) {
"Invalid key $key. Length (${key.length}) is not in the range [$MIN_KEY_LENGTH, $MAX_KEY_LENGTH]."
}
// This is a Firebase requirement
require(!key.first().isDigit()) { "Invalid key $key. Key must not start with a number." }
require(REGEX.matches(key)) { "Invalid key $key. Key must contain only letter and numbers." }
}
}
}

View File

@ -0,0 +1,29 @@
package co.electriccoin.zcash.configuration.model.entry
import co.electriccoin.zcash.configuration.model.map.Configuration
/**
* An entry represents a key and a default value for the configuration. By using an entry object,
* multiple parts of the code can fetch the same configuration without duplication or accidental
* variation in default value. Clients define the key and default value together, rather than just
* the key.
*/
/*
* API note: the default value is not available through the public interface in order to prevent
* clients from accidentally using the default value instead of the configuration value.
*
* Implementation note: although primitives would be nice, Objects don't increase memory usage much.
* The autoboxing cache solves Booleans, and Strings are already objects, so that just leaves Integers.
* Overall the number of Integer configuration entries is expected to be low compared to Booleans,
* and perhaps many Integer values will also fit within the autoboxing cache.
*/
interface DefaultEntry<T> {
val key: ConfigKey
/**
* @param configuration Configuration mapping to check for the key given to this entry.
* @return The value in the configuration, or the default value if no mapping exists.
*/
fun getValue(configuration: Configuration): T
}

View File

@ -0,0 +1,11 @@
package co.electriccoin.zcash.configuration.model.entry
import co.electriccoin.zcash.configuration.model.map.Configuration
data class IntegerConfigurationEntry(
override val key: ConfigKey,
private val defaultValue: Int
) : DefaultEntry<Int> {
override fun getValue(configuration: Configuration) = configuration.getInt(key, defaultValue)
}

View File

@ -0,0 +1,11 @@
package co.electriccoin.zcash.configuration.model.entry
import co.electriccoin.zcash.configuration.model.map.Configuration
data class StringConfigurationEntry(
override val key: ConfigKey,
private val defaultValue: String
) : DefaultEntry<String> {
override fun getValue(configuration: Configuration) = configuration.getString(key, defaultValue)
}

View File

@ -0,0 +1,47 @@
package co.electriccoin.zcash.configuration.model.map
import co.electriccoin.zcash.configuration.model.entry.ConfigKey
import kotlinx.datetime.Instant
/**
* An immutable snapshot of a key-value configuration.
*/
interface Configuration {
/**
* @return When the configuration was updated. Null indicates the configuration either doesn't refresh or has never been refreshed.
*/
val updatedAt: Instant?
/**
* @param key Key to check.
* @return True if a mapping for `key` exists.
*/
fun hasKey(key: ConfigKey): Boolean
/**
* @param key Key to use to retrieve the value.
* @param defaultValue Value to use if `key` doesn't exist in the
* configuration. Some implementations may not use strong types, and the default can also
* be returned if type coercion fails.
* @return boolean mapping for `key` or `defaultValue`.
*/
fun getBoolean(key: ConfigKey, defaultValue: Boolean): Boolean
/**
* @param key Key to use to retrieve the value.
* @param defaultValue Value to use if `key` doesn't exist in the
* configuration. Some implementations may not use strong types, and the default can also
* be returned if type coercion fails.
* @return int mapping for `key` or `defaultValue`.
*/
fun getInt(key: ConfigKey, defaultValue: Int): Int
/**
* @param key Key to use to retrieve the value.
* @param defaultValue Value to use if `key` doesn't exist in the
* configuration. Some implementations may not use strong types, and the default can also
* be returned if type coercion fails.
* @return String mapping for `key` or `defaultValue`.
*/
fun getString(key: ConfigKey, defaultValue: String): String
}

View File

@ -0,0 +1,35 @@
package co.electriccoin.zcash.configuration.model.map
import co.electriccoin.zcash.configuration.model.entry.ConfigKey
import kotlinx.collections.immutable.PersistentMap
import kotlinx.datetime.Instant
// The configurationMapping is intended to be a public API for configuration implementations rather
// than a public API for configuration clients.
data class StringConfiguration(val configurationMapping: PersistentMap<String, String>, override val updatedAt: Instant?) : Configuration {
override fun getBoolean(
key: ConfigKey,
defaultValue: Boolean
) = configurationMapping[key.key]?.let {
try {
it.toBooleanStrict()
} catch (@Suppress("SwallowedException") e: IllegalArgumentException) {
// In the future, log coercion failure as this could mean someone made an error in the remote config console
defaultValue
}
} ?: defaultValue
override fun getInt(key: ConfigKey, defaultValue: Int) = configurationMapping[key.key]?.let {
try {
it.toInt()
} catch (@Suppress("SwallowedException") e: NumberFormatException) {
// In the future, log coercion failure as this could mean someone made an error in the remote config console
defaultValue
}
} ?: defaultValue
override fun getString(key: ConfigKey, defaultValue: String) = configurationMapping.getOrElse(key.key) { defaultValue }
override fun hasKey(key: ConfigKey) = configurationMapping.containsKey(key.key)
}

View File

@ -0,0 +1,75 @@
package co.electriccoin.zcash.configuration.api
import co.electriccoin.zcash.configuration.model.map.Configuration
import co.electriccoin.zcash.configuration.model.map.StringConfiguration
import co.electriccoin.zcash.configuration.test.fixture.BooleanDefaultEntryFixture
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.persistentMapOf
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.test.runTest
import kotlinx.datetime.toInstant
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertTrue
class MergingConfigurationProviderTest {
@Test
fun peek_ordering() {
val configurationProvider = MergingConfigurationProvider(
persistentListOf(
MockConfigurationProvider(StringConfiguration(persistentMapOf(BooleanDefaultEntryFixture.KEY.key to true.toString()), null)),
MockConfigurationProvider(StringConfiguration(persistentMapOf(BooleanDefaultEntryFixture.KEY.key to false.toString()), null))
)
)
assertTrue(BooleanDefaultEntryFixture.newTrueEntry().getValue(configurationProvider.peekConfiguration()))
}
@Test
@OptIn(ExperimentalCoroutinesApi::class)
fun getFlow_ordering() = runTest {
val configurationProvider = MergingConfigurationProvider(
persistentListOf(
MockConfigurationProvider(StringConfiguration(persistentMapOf(BooleanDefaultEntryFixture.KEY.key to true.toString()), null)),
MockConfigurationProvider(StringConfiguration(persistentMapOf(BooleanDefaultEntryFixture.KEY.key to false.toString()), null))
)
)
assertTrue(BooleanDefaultEntryFixture.newTrueEntry().getValue(configurationProvider.getConfigurationFlow().first()))
}
@Test
@OptIn(ExperimentalCoroutinesApi::class)
fun getUpdatedAt_newest() = runTest {
val older = "2023-01-15T08:38:45.415Z".toInstant()
val newer = "2023-01-17T08:38:45.415Z".toInstant()
val configurationProvider = MergingConfigurationProvider(
persistentListOf(
MockConfigurationProvider(StringConfiguration(persistentMapOf(BooleanDefaultEntryFixture.KEY.key to true.toString()), older)),
MockConfigurationProvider(StringConfiguration(persistentMapOf(BooleanDefaultEntryFixture.KEY.key to false.toString()), newer))
)
)
val updatedAt = configurationProvider.getConfigurationFlow().first().updatedAt
assertEquals(newer, updatedAt)
}
}
private class MockConfigurationProvider(private val configuration: Configuration) : ConfigurationProvider {
override fun peekConfiguration(): Configuration {
return configuration
}
override fun getConfigurationFlow(): Flow<Configuration> {
return flowOf(configuration)
}
override fun hintToRefresh() {
// no-op
}
}

View File

@ -0,0 +1,41 @@
package co.electriccoin.zcash.configuration.model.entry
import co.electriccoin.zcash.configuration.test.MockConfiguration
import co.electriccoin.zcash.configuration.test.fixture.BooleanDefaultEntryFixture
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertFalse
import kotlin.test.assertTrue
class BooleanDefaultEntryTest {
@Test
fun key() {
assertEquals(BooleanDefaultEntryFixture.KEY, BooleanDefaultEntryFixture.newTrueEntry().key)
}
@Test
fun value_default_true() {
val entry = BooleanDefaultEntryFixture.newTrueEntry()
assertTrue(entry.getValue(MockConfiguration()))
}
@Test
fun value_default_false() {
val entry = BooleanDefaultEntryFixture.newFalseEntry()
assertFalse(entry.getValue(MockConfiguration()))
}
@Test
fun value_from_config_false() {
val entry = BooleanDefaultEntryFixture.newTrueEntry()
val config = MockConfiguration(mapOf(BooleanDefaultEntryFixture.KEY.key to false.toString()))
assertFalse(entry.getValue(config))
}
@Test
fun value_from_config_true() {
val entry = BooleanDefaultEntryFixture.newTrueEntry()
val config = MockConfiguration(mapOf(BooleanDefaultEntryFixture.KEY.key to true.toString()))
assertTrue(entry.getValue(config))
}
}

View File

@ -0,0 +1,27 @@
package co.electriccoin.zcash.configuration.model.entry
import co.electriccoin.zcash.configuration.test.MockConfiguration
import co.electriccoin.zcash.configuration.test.fixture.IntegerDefaultEntryFixture
import kotlin.test.Test
import kotlin.test.assertEquals
class IntegerDefaultEntryTest {
@Test
fun key() {
assertEquals(IntegerDefaultEntryFixture.KEY, IntegerDefaultEntryFixture.newEntry().key)
}
@Test
fun value_default() {
val entry = IntegerDefaultEntryFixture.newEntry()
assertEquals(IntegerDefaultEntryFixture.DEFAULT_VALUE, entry.getValue(MockConfiguration()))
}
@Test
fun value_override() {
val expected = IntegerDefaultEntryFixture.DEFAULT_VALUE + 5
val entry = IntegerDefaultEntryFixture.newEntry()
assertEquals(expected, entry.getValue(MockConfiguration(mapOf(IntegerDefaultEntryFixture.KEY.key to expected.toString()))))
}
}

View File

@ -0,0 +1,25 @@
package co.electriccoin.zcash.configuration.model.entry
import co.electriccoin.zcash.configuration.test.MockConfiguration
import co.electriccoin.zcash.configuration.test.fixture.StringDefaultEntryFixture
import kotlin.test.Test
import kotlin.test.assertEquals
class StringDefaultEntryTest {
@Test
fun key() {
assertEquals(StringDefaultEntryFixture.KEY, StringDefaultEntryFixture.newEntryEntry().key)
}
@Test
fun value_default() {
val entry = StringDefaultEntryFixture.newEntryEntry()
assertEquals(StringDefaultEntryFixture.DEFAULT_VALUE, entry.getValue(MockConfiguration()))
}
@Test
fun value_override() {
val entry = StringDefaultEntryFixture.newEntryEntry()
assertEquals("override", entry.getValue(MockConfiguration(mapOf(StringDefaultEntryFixture.KEY.key to "override"))))
}
}

View File

@ -0,0 +1,41 @@
package co.electriccoin.zcash.configuration.test
import co.electriccoin.zcash.configuration.model.entry.ConfigKey
import co.electriccoin.zcash.configuration.model.map.Configuration
import kotlinx.datetime.Instant
/**
* @param configurationMapping A mapping of key-value pairs to be returned
* by [.getString]. Note: this map is not defensively copied, allowing users of this class to
* mutate the configuration by mutating the original map. The mapping is stored in a val field
* though, making the initial mapping thread-safe.
*/
class MockConfiguration(private val configurationMapping: Map<String, String> = emptyMap()) : Configuration {
override val updatedAt: Instant? = null
override fun getBoolean(
key: ConfigKey,
defaultValue: Boolean
) = configurationMapping[key.key]?.let {
try {
it.toBooleanStrict()
} catch (@Suppress("SwallowedException") e: IllegalArgumentException) {
// In the future, log coercion failure as this could mean someone made an error in the remote config console
defaultValue
}
} ?: defaultValue
override fun getInt(key: ConfigKey, defaultValue: Int) = configurationMapping[key.key]?.let {
try {
it.toInt()
} catch (@Suppress("SwallowedException") e: NumberFormatException) {
// In the future, log coercion failure as this could mean someone made an error in the remote config console
defaultValue
}
} ?: defaultValue
override fun getString(key: ConfigKey, defaultValue: String) = configurationMapping.getOrElse(key.key) { defaultValue }
override fun hasKey(key: ConfigKey) = configurationMapping.containsKey(key.key)
}

View File

@ -0,0 +1,13 @@
package co.electriccoin.zcash.configuration.test.fixture
import co.electriccoin.zcash.configuration.model.entry.BooleanConfigurationEntry
import co.electriccoin.zcash.configuration.model.entry.ConfigKey
object BooleanDefaultEntryFixture {
val KEY = ConfigKey("some_boolean_key") // $NON-NLS
fun newTrueEntry() = BooleanConfigurationEntry(KEY, true)
fun newFalseEntry() = BooleanConfigurationEntry(KEY, false)
}

View File

@ -0,0 +1,10 @@
package co.electriccoin.zcash.configuration.test.fixture
import co.electriccoin.zcash.configuration.model.entry.ConfigKey
import co.electriccoin.zcash.configuration.model.entry.IntegerConfigurationEntry
object IntegerDefaultEntryFixture {
val KEY = ConfigKey("some_string_key") // $NON-NLS
const val DEFAULT_VALUE = 123
fun newEntry(key: ConfigKey = KEY, value: Int = DEFAULT_VALUE) = IntegerConfigurationEntry(key, value)
}

View File

@ -0,0 +1,10 @@
package co.electriccoin.zcash.configuration.test.fixture
import co.electriccoin.zcash.configuration.model.entry.ConfigKey
import co.electriccoin.zcash.configuration.model.entry.StringConfigurationEntry
object StringDefaultEntryFixture {
val KEY = ConfigKey("some_string_key") // $NON-NLS
const val DEFAULT_VALUE = "some_default_value" // $NON-NLS
fun newEntryEntry(key: ConfigKey = KEY, value: String = DEFAULT_VALUE) = StringConfigurationEntry(key, value)
}

View File

@ -26,6 +26,7 @@ The logical components of the app are implemented as a number of Gradle modules.
* `app` — Compiles all the modules together into the final application. This module contains minimal actual code. Note that the Java package structure for this module is under `co.electriccoin.zcash.app` while the Android package name is `co.electriccoin.zcash`.
* `build-info-lib` — Collects information from the build environment (e.g. Git SHA, Git commit count) and compiles them into the application. Can also be used for injection of API keys or other secrets.
* `configuration-api-lib` — Multiplatform interfaces for remote configuration.
* crash — For collecting and reporting exceptions and crashes
* `crash-lib` — Common crash collection logic for Kotlin and JVM. This is not fully-featured by itself, but the long-term plan is multiplatform support.
* `crash-android-lib` — Android-specific crash collection logic, built on top of the common and JVM implementation in `crash-lib`

View File

@ -144,6 +144,7 @@ JACOCO_VERSION=0.8.8
KOTLIN_VERSION=1.8.10
KOTLINX_COROUTINES_VERSION=1.6.4
KOTLINX_DATETIME_VERSION=0.4.0
KOTLINX_IMMUTABLE_COLLECTIONS_VERSION=0.3.5
KOVER_VERSION=0.6.1
PLAY_APP_UPDATE_VERSION=2.0.1
PLAY_APP_UPDATE_KTX_VERSION=2.0.1

View File

@ -1,19 +1,15 @@
package co.electriccoin.zcash.preference.api
import co.electriccoin.zcash.preference.model.entry.Key
import co.electriccoin.zcash.preference.model.entry.PreferenceKey
import kotlinx.coroutines.flow.Flow
interface PreferenceProvider {
suspend fun hasKey(key: Key): Boolean
suspend fun hasKey(key: PreferenceKey): Boolean
suspend fun putString(key: Key, value: String?)
suspend fun putString(key: PreferenceKey, value: String?)
suspend fun getString(key: Key): String?
suspend fun getString(key: PreferenceKey): String?
/**
* @return Flow to observe potential changes to the value associated with the key in the preferences.
* Consumers of the flow will need to then query the value and determine whether it has changed.
*/
fun observe(key: Key): Flow<Unit>
fun observe(key: PreferenceKey): Flow<String?>
}

View File

@ -3,7 +3,7 @@ package co.electriccoin.zcash.preference.model.entry
import co.electriccoin.zcash.preference.api.PreferenceProvider
data class BooleanPreferenceDefault(
override val key: Key,
override val key: PreferenceKey,
private val defaultValue: Boolean
) : PreferenceDefault<Boolean> {

View File

@ -3,7 +3,7 @@ package co.electriccoin.zcash.preference.model.entry
import co.electriccoin.zcash.preference.api.PreferenceProvider
data class IntegerPreferenceDefault(
override val key: Key,
override val key: PreferenceKey,
private val defaultValue: Int
) : PreferenceDefault<Int> {

View File

@ -22,7 +22,7 @@ import kotlinx.coroutines.flow.map
*/
interface PreferenceDefault<T> {
val key: Key
val key: PreferenceKey
/**
* @param preferenceProvider Provides actual preference values.

View File

@ -9,7 +9,7 @@ import kotlin.jvm.JvmInline
* find a least common denominator with some reasonable limits on what the keys can contain.
*/
@JvmInline
value class Key(val key: String) {
value class PreferenceKey(val key: String) {
init {
requireKeyConstraints(key)
}

View File

@ -3,7 +3,7 @@ package co.electriccoin.zcash.preference.model.entry
import co.electriccoin.zcash.preference.api.PreferenceProvider
data class StringPreferenceDefault(
override val key: Key,
override val key: PreferenceKey,
private val defaultValue: String
) : PreferenceDefault<String> {

View File

@ -1,9 +1,9 @@
package co.electriccoin.zcash.preference.test
import co.electriccoin.zcash.preference.api.PreferenceProvider
import co.electriccoin.zcash.preference.model.entry.Key
import co.electriccoin.zcash.preference.model.entry.PreferenceKey
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.flow
/**
* @param mutableMapFactory Emits a new mutable map. Thread safety depends on the factory implementation.
@ -12,14 +12,14 @@ class MockPreferenceProvider(mutableMapFactory: () -> MutableMap<String, String?
private val map = mutableMapFactory()
override suspend fun getString(key: Key) = map[key.key]
override suspend fun getString(key: PreferenceKey) = map[key.key]
// For the mock implementation, does not support observability of changes
override fun observe(key: Key): Flow<Unit> = flowOf(Unit)
override fun observe(key: PreferenceKey): Flow<String?> = flow { emit(getString(key)) }
override suspend fun hasKey(key: Key) = map.containsKey(key.key)
override suspend fun hasKey(key: PreferenceKey) = map.containsKey(key.key)
override suspend fun putString(key: Key, value: String?) {
override suspend fun putString(key: PreferenceKey, value: String?) {
map[key.key] = value
}
}

View File

@ -1,10 +1,10 @@
package co.electriccoin.zcash.preference.test.fixture
import co.electriccoin.zcash.preference.model.entry.BooleanPreferenceDefault
import co.electriccoin.zcash.preference.model.entry.Key
import co.electriccoin.zcash.preference.model.entry.PreferenceKey
object BooleanPreferenceDefaultFixture {
val KEY = Key("some_boolean_key") // $NON-NLS
val KEY = PreferenceKey("some_boolean_key") // $NON-NLS
fun newTrue() = BooleanPreferenceDefault(KEY, true)
fun newFalse() = BooleanPreferenceDefault(KEY, false)
}

View File

@ -1,10 +1,10 @@
package co.electriccoin.zcash.preference.test.fixture
import co.electriccoin.zcash.preference.model.entry.IntegerPreferenceDefault
import co.electriccoin.zcash.preference.model.entry.Key
import co.electriccoin.zcash.preference.model.entry.PreferenceKey
object IntegerPreferenceDefaultFixture {
val KEY = Key("some_string_key") // $NON-NLS
val KEY = PreferenceKey("some_string_key") // $NON-NLS
const val DEFAULT_VALUE = 123
fun new(key: Key = KEY, value: Int = DEFAULT_VALUE) = IntegerPreferenceDefault(key, value)
fun new(preferenceKey: PreferenceKey = KEY, value: Int = DEFAULT_VALUE) = IntegerPreferenceDefault(preferenceKey, value)
}

View File

@ -1,10 +1,10 @@
package co.electriccoin.zcash.preference.test.fixture
import co.electriccoin.zcash.preference.model.entry.Key
import co.electriccoin.zcash.preference.model.entry.PreferenceKey
import co.electriccoin.zcash.preference.model.entry.StringPreferenceDefault
object StringDefaultPreferenceFixture {
val KEY = Key("some_string_key") // $NON-NLS
val KEY = PreferenceKey("some_string_key") // $NON-NLS
const val DEFAULT_VALUE = "some_default_value" // $NON-NLS
fun new(key: Key = KEY, value: String = DEFAULT_VALUE) = StringPreferenceDefault(key, value)
fun new(preferenceKey: PreferenceKey = KEY, value: String = DEFAULT_VALUE) = StringPreferenceDefault(preferenceKey, value)
}

View File

@ -1,10 +1,10 @@
package co.electriccoin.zcash.preference.test.fixture
import co.electriccoin.zcash.preference.model.entry.Key
import co.electriccoin.zcash.preference.model.entry.PreferenceKey
import co.electriccoin.zcash.preference.model.entry.StringPreferenceDefault
object StringDefaultPreferenceFixture {
val KEY = Key("some_string_key") // $NON-NLS
val KEY = PreferenceKey("some_string_key") // $NON-NLS
const val DEFAULT_VALUE = "some_default_value" // $NON-NLS
fun new(key: Key = KEY, value: String = DEFAULT_VALUE) = StringPreferenceDefault(key, value)
fun new(preferenceKey: PreferenceKey = KEY, value: String = DEFAULT_VALUE) = StringPreferenceDefault(preferenceKey, value)
}

View File

@ -6,14 +6,14 @@ import android.content.SharedPreferences
import androidx.security.crypto.EncryptedSharedPreferences
import androidx.security.crypto.MasterKey
import co.electriccoin.zcash.preference.api.PreferenceProvider
import co.electriccoin.zcash.preference.model.entry.Key
import co.electriccoin.zcash.preference.model.entry.PreferenceKey
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.asCoroutineDispatcher
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.withContext
import java.util.concurrent.Executors
@ -35,12 +35,12 @@ class AndroidPreferenceProvider(
private val dispatcher: CoroutineDispatcher
) : PreferenceProvider {
override suspend fun hasKey(key: Key) = withContext(dispatcher) {
override suspend fun hasKey(key: PreferenceKey) = withContext(dispatcher) {
sharedPreferences.contains(key.key)
}
@SuppressLint("ApplySharedPref")
override suspend fun putString(key: Key, value: String?) = withContext(dispatcher) {
override suspend fun putString(key: PreferenceKey, value: String?) = withContext(dispatcher) {
val editor = sharedPreferences.edit()
editor.putString(key.key, value)
@ -50,12 +50,11 @@ class AndroidPreferenceProvider(
Unit
}
override suspend fun getString(key: Key) = withContext(dispatcher) {
override suspend fun getString(key: PreferenceKey) = withContext(dispatcher) {
sharedPreferences.getString(key.key, null)
}
@OptIn(ExperimentalCoroutinesApi::class)
override fun observe(key: Key): Flow<Unit> = callbackFlow<Unit> {
override fun observe(key: PreferenceKey): Flow<String?> = callbackFlow<Unit> {
val listener = SharedPreferences.OnSharedPreferenceChangeListener { _, _ ->
// Callback on main thread
trySend(Unit)
@ -69,6 +68,7 @@ class AndroidPreferenceProvider(
sharedPreferences.unregisterOnSharedPreferenceChangeListener(listener)
}
}.flowOn(dispatcher)
.map { getString(key) }
companion object {
suspend fun newStandard(context: Context, filename: String): PreferenceProvider {

View File

@ -174,6 +174,7 @@ dependencyResolutionManagement {
val kotlinVersion = extra["KOTLIN_VERSION"].toString()
val kotlinxDateTimeVersion = extra["KOTLINX_DATETIME_VERSION"].toString()
val kotlinxCoroutinesVersion = extra["KOTLINX_COROUTINES_VERSION"].toString()
val kotlinxImmutableCollectionsVersion = extra["KOTLINX_IMMUTABLE_COLLECTIONS_VERSION"].toString()
val playAppUpdateVersion = extra["PLAY_APP_UPDATE_VERSION"].toString()
val playAppUpdateKtxVersion = extra["PLAY_APP_UPDATE_KTX_VERSION"].toString()
val zcashBip39Version = extra["ZCASH_BIP39_VERSION"].toString()
@ -225,6 +226,7 @@ dependencyResolutionManagement {
library("kotlinx-coroutines-core", "org.jetbrains.kotlinx:kotlinx-coroutines-core:$kotlinxCoroutinesVersion")
library("kotlinx-coroutines-guava", "org.jetbrains.kotlinx:kotlinx-coroutines-guava:$kotlinxCoroutinesVersion")
library("kotlinx-datetime", "org.jetbrains.kotlinx:kotlinx-datetime:$kotlinxDateTimeVersion")
library("kotlinx-immutable", "org.jetbrains.kotlinx:kotlinx-collections-immutable:$kotlinxImmutableCollectionsVersion")
library("play-update", "com.google.android.play:app-update:$playAppUpdateVersion")
library("play-update-ktx", "com.google.android.play:app-update-ktx:$playAppUpdateKtxVersion")
library("zcash-sdk", "cash.z.ecc.android:zcash-android-sdk:$zcashSdkVersion")
@ -307,6 +309,7 @@ includeBuild("build-conventions-secant")
include("app")
include("build-info-lib")
include("configuration-api-lib")
include("crash-lib")
include("crash-android-lib")
include("preference-api-lib")

View File

@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<permission android:name="android.permission.WAKE_LOCK"/>
<uses-permission android:name="android.permission.WAKE_LOCK"/>
<application />

View File

@ -1,8 +1,8 @@
package co.electriccoin.zcash.ui.preference
import co.electriccoin.zcash.preference.model.entry.Key
import co.electriccoin.zcash.preference.model.entry.PreferenceKey
object EncryptedPreferenceKeys {
val PERSISTABLE_WALLET = PersistableWalletPreferenceDefault(Key("persistable_wallet"))
val PERSISTABLE_WALLET = PersistableWalletPreferenceDefault(PreferenceKey("persistable_wallet"))
}

View File

@ -2,11 +2,11 @@ package co.electriccoin.zcash.ui.preference
import cash.z.ecc.android.sdk.model.FiatCurrency
import co.electriccoin.zcash.preference.api.PreferenceProvider
import co.electriccoin.zcash.preference.model.entry.Key
import co.electriccoin.zcash.preference.model.entry.PreferenceDefault
import co.electriccoin.zcash.preference.model.entry.PreferenceKey
data class FiatCurrencyPreferenceDefault(
override val key: Key
override val key: PreferenceKey
) : PreferenceDefault<FiatCurrency> {
override suspend fun getValue(preferenceProvider: PreferenceProvider) =

View File

@ -2,12 +2,12 @@ package co.electriccoin.zcash.ui.preference
import cash.z.ecc.android.sdk.model.PersistableWallet
import co.electriccoin.zcash.preference.api.PreferenceProvider
import co.electriccoin.zcash.preference.model.entry.Key
import co.electriccoin.zcash.preference.model.entry.PreferenceDefault
import co.electriccoin.zcash.preference.model.entry.PreferenceKey
import org.json.JSONObject
data class PersistableWalletPreferenceDefault(
override val key: Key
override val key: PreferenceKey
) : PreferenceDefault<PersistableWallet?> {
override suspend fun getValue(preferenceProvider: PreferenceProvider) =

View File

@ -1,24 +1,24 @@
package co.electriccoin.zcash.ui.preference
import co.electriccoin.zcash.preference.model.entry.BooleanPreferenceDefault
import co.electriccoin.zcash.preference.model.entry.Key
import co.electriccoin.zcash.preference.model.entry.PreferenceKey
object StandardPreferenceKeys {
/**
* Whether the user has completed the backup flow for a newly created wallet.
*/
val IS_USER_BACKUP_COMPLETE = BooleanPreferenceDefault(Key("is_user_backup_complete"), false)
val IS_USER_BACKUP_COMPLETE = BooleanPreferenceDefault(PreferenceKey("is_user_backup_complete"), false)
// Default to true until https://github.com/zcash/secant-android-wallet/issues/304
val IS_ANALYTICS_ENABLED = BooleanPreferenceDefault(Key("is_analytics_enabled"), true)
val IS_ANALYTICS_ENABLED = BooleanPreferenceDefault(PreferenceKey("is_analytics_enabled"), true)
val IS_BACKGROUND_SYNC_ENABLED = BooleanPreferenceDefault(Key("is_background_sync_enabled"), true)
val IS_BACKGROUND_SYNC_ENABLED = BooleanPreferenceDefault(PreferenceKey("is_background_sync_enabled"), true)
val IS_KEEP_SCREEN_ON_DURING_SYNC = BooleanPreferenceDefault(Key("is_keep_screen_on_during_sync"), true)
val IS_KEEP_SCREEN_ON_DURING_SYNC = BooleanPreferenceDefault(PreferenceKey("is_keep_screen_on_during_sync"), true)
/**
* The fiat currency that the user prefers.
*/
val PREFERRED_FIAT_CURRENCY = FiatCurrencyPreferenceDefault(Key("preferred_fiat_currency_code"))
val PREFERRED_FIAT_CURRENCY = FiatCurrencyPreferenceDefault(PreferenceKey("preferred_fiat_currency_code"))
}