[#1108] Ktlint 1.0.1
* [#1108] Ktlint 1.0.1 - Closes #1108 - Version and artefact update * Fix ktlint warnings
This commit is contained in:
parent
a10b372e73
commit
a3e7d8f6c4
|
@ -10,7 +10,6 @@ import org.junit.Assert.assertEquals
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
|
||||||
class AndroidApiTest {
|
class AndroidApiTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@SmallTest
|
@SmallTest
|
||||||
fun checkTargetApi() {
|
fun checkTargetApi() {
|
||||||
|
|
|
@ -9,7 +9,6 @@ import kotlinx.coroutines.launch
|
||||||
|
|
||||||
@Suppress("unused")
|
@Suppress("unused")
|
||||||
class ZcashApplication : CoroutineApplication() {
|
class ZcashApplication : CoroutineApplication() {
|
||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
|
|
||||||
|
|
|
@ -5,7 +5,7 @@ plugins {
|
||||||
val ktlint by configurations.creating
|
val ktlint by configurations.creating
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
ktlint("com.pinterest:ktlint:${project.property("KTLINT_VERSION")}") {
|
ktlint("com.pinterest.ktlint:ktlint-cli:${project.property("KTLINT_VERSION")}") {
|
||||||
attributes {
|
attributes {
|
||||||
attribute(Bundling.BUNDLING_ATTRIBUTE, objects.named<Bundling>(Bundling.EXTERNAL))
|
attribute(Bundling.BUNDLING_ATTRIBUTE, objects.named<Bundling>(Bundling.EXTERNAL))
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,7 +7,6 @@ import kotlinx.coroutines.flow.Flow
|
||||||
* Provides a remote config implementation.
|
* Provides a remote config implementation.
|
||||||
*/
|
*/
|
||||||
interface ConfigurationProvider {
|
interface ConfigurationProvider {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return The configuration if it has been loaded already. If not loaded, returns an empty configuration.
|
* @return The configuration if it has been loaded already. If not loaded, returns an empty configuration.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -40,19 +40,28 @@ private data class MergingConfiguration(private val configurations: PersistentLi
|
||||||
return null != configurations.firstWithKey(key)
|
return null != configurations.firstWithKey(key)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getBoolean(key: ConfigKey, defaultValue: Boolean): Boolean {
|
override fun getBoolean(
|
||||||
|
key: ConfigKey,
|
||||||
|
defaultValue: Boolean
|
||||||
|
): Boolean {
|
||||||
return configurations.firstWithKey(key)?.let {
|
return configurations.firstWithKey(key)?.let {
|
||||||
return it.getBoolean(key, defaultValue)
|
return it.getBoolean(key, defaultValue)
|
||||||
} ?: defaultValue
|
} ?: defaultValue
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getInt(key: ConfigKey, defaultValue: Int): Int {
|
override fun getInt(
|
||||||
|
key: ConfigKey,
|
||||||
|
defaultValue: Int
|
||||||
|
): Int {
|
||||||
return configurations.firstWithKey(key)?.let {
|
return configurations.firstWithKey(key)?.let {
|
||||||
return it.getInt(key, defaultValue)
|
return it.getInt(key, defaultValue)
|
||||||
} ?: defaultValue
|
} ?: defaultValue
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getString(key: ConfigKey, defaultValue: String): String {
|
override fun getString(
|
||||||
|
key: ConfigKey,
|
||||||
|
defaultValue: String
|
||||||
|
): String {
|
||||||
return configurations.firstWithKey(key)?.let {
|
return configurations.firstWithKey(key)?.let {
|
||||||
return it.getString(key, defaultValue)
|
return it.getString(key, defaultValue)
|
||||||
} ?: defaultValue
|
} ?: defaultValue
|
||||||
|
|
|
@ -6,7 +6,5 @@ data class BooleanConfigurationEntry(
|
||||||
override val key: ConfigKey,
|
override val key: ConfigKey,
|
||||||
private val defaultValue: Boolean
|
private val defaultValue: Boolean
|
||||||
) : DefaultEntry<Boolean> {
|
) : DefaultEntry<Boolean> {
|
||||||
|
override fun getValue(configuration: Configuration) = configuration.getBoolean(key, defaultValue)
|
||||||
override fun getValue(configuration: Configuration) =
|
|
||||||
configuration.getBoolean(key, defaultValue)
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,7 +8,8 @@ import co.electriccoin.zcash.configuration.model.map.Configuration
|
||||||
* variation in default value. Clients define the key and default value together, rather than just
|
* variation in default value. Clients define the key and default value together, rather than just
|
||||||
* the key.
|
* the key.
|
||||||
*/
|
*/
|
||||||
/*
|
interface DefaultEntry<T> {
|
||||||
|
/*
|
||||||
* API note: the default value is not available through the public interface in order to prevent
|
* 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.
|
* clients from accidentally using the default value instead of the configuration value.
|
||||||
*
|
*
|
||||||
|
@ -17,7 +18,6 @@ import co.electriccoin.zcash.configuration.model.map.Configuration
|
||||||
* Overall the number of Integer configuration entries is expected to be low compared to Booleans,
|
* 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.
|
* and perhaps many Integer values will also fit within the autoboxing cache.
|
||||||
*/
|
*/
|
||||||
interface DefaultEntry<T> {
|
|
||||||
|
|
||||||
val key: ConfigKey
|
val key: ConfigKey
|
||||||
|
|
||||||
|
|
|
@ -6,6 +6,5 @@ data class IntegerConfigurationEntry(
|
||||||
override val key: ConfigKey,
|
override val key: ConfigKey,
|
||||||
private val defaultValue: Int
|
private val defaultValue: Int
|
||||||
) : DefaultEntry<Int> {
|
) : DefaultEntry<Int> {
|
||||||
|
|
||||||
override fun getValue(configuration: Configuration) = configuration.getInt(key, defaultValue)
|
override fun getValue(configuration: Configuration) = configuration.getInt(key, defaultValue)
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,6 +6,5 @@ data class StringConfigurationEntry(
|
||||||
override val key: ConfigKey,
|
override val key: ConfigKey,
|
||||||
private val defaultValue: String
|
private val defaultValue: String
|
||||||
) : DefaultEntry<String> {
|
) : DefaultEntry<String> {
|
||||||
|
|
||||||
override fun getValue(configuration: Configuration) = configuration.getString(key, defaultValue)
|
override fun getValue(configuration: Configuration) = configuration.getString(key, defaultValue)
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,7 +26,10 @@ interface Configuration {
|
||||||
* be returned if type coercion fails.
|
* be returned if type coercion fails.
|
||||||
* @return boolean mapping for `key` or `defaultValue`.
|
* @return boolean mapping for `key` or `defaultValue`.
|
||||||
*/
|
*/
|
||||||
fun getBoolean(key: ConfigKey, defaultValue: Boolean): Boolean
|
fun getBoolean(
|
||||||
|
key: ConfigKey,
|
||||||
|
defaultValue: Boolean
|
||||||
|
): Boolean
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param key Key to use to retrieve the value.
|
* @param key Key to use to retrieve the value.
|
||||||
|
@ -35,7 +38,10 @@ interface Configuration {
|
||||||
* be returned if type coercion fails.
|
* be returned if type coercion fails.
|
||||||
* @return int mapping for `key` or `defaultValue`.
|
* @return int mapping for `key` or `defaultValue`.
|
||||||
*/
|
*/
|
||||||
fun getInt(key: ConfigKey, defaultValue: Int): Int
|
fun getInt(
|
||||||
|
key: ConfigKey,
|
||||||
|
defaultValue: Int
|
||||||
|
): Int
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param key Key to use to retrieve the value.
|
* @param key Key to use to retrieve the value.
|
||||||
|
@ -44,5 +50,8 @@ interface Configuration {
|
||||||
* be returned if type coercion fails.
|
* be returned if type coercion fails.
|
||||||
* @return String mapping for `key` or `defaultValue`.
|
* @return String mapping for `key` or `defaultValue`.
|
||||||
*/
|
*/
|
||||||
fun getString(key: ConfigKey, defaultValue: String): String
|
fun getString(
|
||||||
|
key: ConfigKey,
|
||||||
|
defaultValue: String
|
||||||
|
): String
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,30 +10,38 @@ data class StringConfiguration(
|
||||||
val configurationMapping: PersistentMap<String, String>,
|
val configurationMapping: PersistentMap<String, String>,
|
||||||
override val updatedAt: Instant?
|
override val updatedAt: Instant?
|
||||||
) : Configuration {
|
) : Configuration {
|
||||||
|
|
||||||
override fun getBoolean(
|
override fun getBoolean(
|
||||||
key: ConfigKey,
|
key: ConfigKey,
|
||||||
defaultValue: Boolean
|
defaultValue: Boolean
|
||||||
) = configurationMapping[key.key]?.let {
|
) = configurationMapping[key.key]?.let {
|
||||||
try {
|
try {
|
||||||
it.toBooleanStrict()
|
it.toBooleanStrict()
|
||||||
} catch (@Suppress("SwallowedException") e: IllegalArgumentException) {
|
} catch (
|
||||||
|
@Suppress("SwallowedException") e: IllegalArgumentException
|
||||||
|
) {
|
||||||
// In the future, log coercion failure as this could mean someone made an error in the remote config console
|
// In the future, log coercion failure as this could mean someone made an error in the remote config console
|
||||||
defaultValue
|
defaultValue
|
||||||
}
|
}
|
||||||
} ?: defaultValue
|
} ?: defaultValue
|
||||||
|
|
||||||
override fun getInt(key: ConfigKey, defaultValue: Int) = configurationMapping[key.key]?.let {
|
override fun getInt(
|
||||||
|
key: ConfigKey,
|
||||||
|
defaultValue: Int
|
||||||
|
) = configurationMapping[key.key]?.let {
|
||||||
try {
|
try {
|
||||||
it.toInt()
|
it.toInt()
|
||||||
} catch (@Suppress("SwallowedException") e: NumberFormatException) {
|
} catch (
|
||||||
|
@Suppress("SwallowedException") e: NumberFormatException
|
||||||
|
) {
|
||||||
// In the future, log coercion failure as this could mean someone made an error in the remote config console
|
// In the future, log coercion failure as this could mean someone made an error in the remote config console
|
||||||
defaultValue
|
defaultValue
|
||||||
}
|
}
|
||||||
} ?: defaultValue
|
} ?: defaultValue
|
||||||
|
|
||||||
override fun getString(key: ConfigKey, defaultValue: String) =
|
override fun getString(
|
||||||
configurationMapping.getOrElse(key.key) { defaultValue }
|
key: ConfigKey,
|
||||||
|
defaultValue: String
|
||||||
|
) = configurationMapping.getOrElse(key.key) { defaultValue }
|
||||||
|
|
||||||
override fun hasKey(key: ConfigKey) = configurationMapping.containsKey(key.key)
|
override fun hasKey(key: ConfigKey) = configurationMapping.containsKey(key.key)
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,13 +18,20 @@ import kotlin.test.assertTrue
|
||||||
class MergingConfigurationProviderTest {
|
class MergingConfigurationProviderTest {
|
||||||
@Test
|
@Test
|
||||||
fun peek_ordering() {
|
fun peek_ordering() {
|
||||||
val configurationProvider = MergingConfigurationProvider(
|
val configurationProvider =
|
||||||
|
MergingConfigurationProvider(
|
||||||
persistentListOf(
|
persistentListOf(
|
||||||
MockConfigurationProvider(
|
MockConfigurationProvider(
|
||||||
StringConfiguration(persistentMapOf(BooleanDefaultEntryFixture.KEY.key to true.toString()), null)
|
StringConfiguration(
|
||||||
|
persistentMapOf(BooleanDefaultEntryFixture.KEY.key to true.toString()),
|
||||||
|
null
|
||||||
|
)
|
||||||
),
|
),
|
||||||
MockConfigurationProvider(
|
MockConfigurationProvider(
|
||||||
StringConfiguration(persistentMapOf(BooleanDefaultEntryFixture.KEY.key to false.toString()), null)
|
StringConfiguration(
|
||||||
|
persistentMapOf(BooleanDefaultEntryFixture.KEY.key to false.toString()),
|
||||||
|
null
|
||||||
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
@ -33,14 +40,22 @@ class MergingConfigurationProviderTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun getFlow_ordering() = runTest {
|
fun getFlow_ordering() =
|
||||||
val configurationProvider = MergingConfigurationProvider(
|
runTest {
|
||||||
|
val configurationProvider =
|
||||||
|
MergingConfigurationProvider(
|
||||||
persistentListOf(
|
persistentListOf(
|
||||||
MockConfigurationProvider(
|
MockConfigurationProvider(
|
||||||
StringConfiguration(persistentMapOf(BooleanDefaultEntryFixture.KEY.key to true.toString()), null)
|
StringConfiguration(
|
||||||
|
persistentMapOf(BooleanDefaultEntryFixture.KEY.key to true.toString()),
|
||||||
|
null
|
||||||
|
)
|
||||||
),
|
),
|
||||||
MockConfigurationProvider(
|
MockConfigurationProvider(
|
||||||
StringConfiguration(persistentMapOf(BooleanDefaultEntryFixture.KEY.key to false.toString()), null)
|
StringConfiguration(
|
||||||
|
persistentMapOf(BooleanDefaultEntryFixture.KEY.key to false.toString()),
|
||||||
|
null
|
||||||
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
@ -51,8 +66,10 @@ class MergingConfigurationProviderTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun getFlow_empty() = runTest {
|
fun getFlow_empty() =
|
||||||
val configurationProvider = MergingConfigurationProvider(
|
runTest {
|
||||||
|
val configurationProvider =
|
||||||
|
MergingConfigurationProvider(
|
||||||
emptyList<ConfigurationProvider>().toPersistentList()
|
emptyList<ConfigurationProvider>().toPersistentList()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -62,17 +79,27 @@ class MergingConfigurationProviderTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun getUpdatedAt_newest() = runTest {
|
fun getUpdatedAt_newest() =
|
||||||
|
runTest {
|
||||||
val older = "2023-01-15T08:38:45.415Z".toInstant()
|
val older = "2023-01-15T08:38:45.415Z".toInstant()
|
||||||
val newer = "2023-01-17T08:38:45.415Z".toInstant()
|
val newer = "2023-01-17T08:38:45.415Z".toInstant()
|
||||||
|
|
||||||
val configurationProvider = MergingConfigurationProvider(
|
val configurationProvider =
|
||||||
|
MergingConfigurationProvider(
|
||||||
persistentListOf(
|
persistentListOf(
|
||||||
MockConfigurationProvider(
|
MockConfigurationProvider(
|
||||||
StringConfiguration(persistentMapOf(BooleanDefaultEntryFixture.KEY.key to true.toString()), older)
|
StringConfiguration(
|
||||||
|
persistentMapOf(BooleanDefaultEntryFixture.KEY.key to true.toString()),
|
||||||
|
older
|
||||||
|
)
|
||||||
),
|
),
|
||||||
MockConfigurationProvider(
|
MockConfigurationProvider(
|
||||||
StringConfiguration(persistentMapOf(BooleanDefaultEntryFixture.KEY.key to false.toString()), newer)
|
StringConfiguration(
|
||||||
|
persistentMapOf(
|
||||||
|
BooleanDefaultEntryFixture.KEY.key to false.toString()
|
||||||
|
),
|
||||||
|
newer
|
||||||
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
@ -83,7 +110,6 @@ class MergingConfigurationProviderTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
private class MockConfigurationProvider(private val configuration: Configuration) : ConfigurationProvider {
|
private class MockConfigurationProvider(private val configuration: Configuration) : ConfigurationProvider {
|
||||||
|
|
||||||
override fun peekConfiguration(): Configuration {
|
override fun peekConfiguration(): Configuration {
|
||||||
return configuration
|
return configuration
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,7 +11,6 @@ import kotlinx.datetime.Instant
|
||||||
* though, making the initial mapping thread-safe.
|
* though, making the initial mapping thread-safe.
|
||||||
*/
|
*/
|
||||||
class MockConfiguration(private val configurationMapping: Map<String, String> = emptyMap()) : Configuration {
|
class MockConfiguration(private val configurationMapping: Map<String, String> = emptyMap()) : Configuration {
|
||||||
|
|
||||||
override val updatedAt: Instant? = null
|
override val updatedAt: Instant? = null
|
||||||
|
|
||||||
override fun getBoolean(
|
override fun getBoolean(
|
||||||
|
@ -20,23 +19,32 @@ class MockConfiguration(private val configurationMapping: Map<String, String> =
|
||||||
) = configurationMapping[key.key]?.let {
|
) = configurationMapping[key.key]?.let {
|
||||||
try {
|
try {
|
||||||
it.toBooleanStrict()
|
it.toBooleanStrict()
|
||||||
} catch (@Suppress("SwallowedException") e: IllegalArgumentException) {
|
} catch (
|
||||||
|
@Suppress("SwallowedException") e: IllegalArgumentException
|
||||||
|
) {
|
||||||
// In the future, log coercion failure as this could mean someone made an error in the remote config console
|
// In the future, log coercion failure as this could mean someone made an error in the remote config console
|
||||||
defaultValue
|
defaultValue
|
||||||
}
|
}
|
||||||
} ?: defaultValue
|
} ?: defaultValue
|
||||||
|
|
||||||
override fun getInt(key: ConfigKey, defaultValue: Int) = configurationMapping[key.key]?.let {
|
override fun getInt(
|
||||||
|
key: ConfigKey,
|
||||||
|
defaultValue: Int
|
||||||
|
) = configurationMapping[key.key]?.let {
|
||||||
try {
|
try {
|
||||||
it.toInt()
|
it.toInt()
|
||||||
} catch (@Suppress("SwallowedException") e: NumberFormatException) {
|
} catch (
|
||||||
|
@Suppress("SwallowedException") e: NumberFormatException
|
||||||
|
) {
|
||||||
// In the future, log coercion failure as this could mean someone made an error in the remote config console
|
// In the future, log coercion failure as this could mean someone made an error in the remote config console
|
||||||
defaultValue
|
defaultValue
|
||||||
}
|
}
|
||||||
} ?: defaultValue
|
} ?: defaultValue
|
||||||
|
|
||||||
override fun getString(key: ConfigKey, defaultValue: String) =
|
override fun getString(
|
||||||
configurationMapping.getOrElse(key.key) { defaultValue }
|
key: ConfigKey,
|
||||||
|
defaultValue: String
|
||||||
|
) = configurationMapping.getOrElse(key.key) { defaultValue }
|
||||||
|
|
||||||
override fun hasKey(key: ConfigKey) = configurationMapping.containsKey(key.key)
|
override fun hasKey(key: ConfigKey) = configurationMapping.containsKey(key.key)
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,7 +4,6 @@ import co.electriccoin.zcash.configuration.model.entry.BooleanConfigurationEntry
|
||||||
import co.electriccoin.zcash.configuration.model.entry.ConfigKey
|
import co.electriccoin.zcash.configuration.model.entry.ConfigKey
|
||||||
|
|
||||||
object BooleanDefaultEntryFixture {
|
object BooleanDefaultEntryFixture {
|
||||||
|
|
||||||
val KEY = ConfigKey("some_boolean_key") // $NON-NLS
|
val KEY = ConfigKey("some_boolean_key") // $NON-NLS
|
||||||
|
|
||||||
fun newTrueEntry() = BooleanConfigurationEntry(KEY, true)
|
fun newTrueEntry() = BooleanConfigurationEntry(KEY, true)
|
||||||
|
|
|
@ -6,5 +6,9 @@ import co.electriccoin.zcash.configuration.model.entry.IntegerConfigurationEntry
|
||||||
object IntegerDefaultEntryFixture {
|
object IntegerDefaultEntryFixture {
|
||||||
val KEY = ConfigKey("some_string_key") // $NON-NLS
|
val KEY = ConfigKey("some_string_key") // $NON-NLS
|
||||||
const val DEFAULT_VALUE = 123
|
const val DEFAULT_VALUE = 123
|
||||||
fun newEntry(key: ConfigKey = KEY, value: Int = DEFAULT_VALUE) = IntegerConfigurationEntry(key, value)
|
|
||||||
|
fun newEntry(
|
||||||
|
key: ConfigKey = KEY,
|
||||||
|
value: Int = DEFAULT_VALUE
|
||||||
|
) = IntegerConfigurationEntry(key, value)
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,5 +6,9 @@ import co.electriccoin.zcash.configuration.model.entry.StringConfigurationEntry
|
||||||
object StringDefaultEntryFixture {
|
object StringDefaultEntryFixture {
|
||||||
val KEY = ConfigKey("some_string_key") // $NON-NLS
|
val KEY = ConfigKey("some_string_key") // $NON-NLS
|
||||||
const val DEFAULT_VALUE = "some_default_value" // $NON-NLS
|
const val DEFAULT_VALUE = "some_default_value" // $NON-NLS
|
||||||
fun newEntryEntry(key: ConfigKey = KEY, value: String = DEFAULT_VALUE) = StringConfigurationEntry(key, value)
|
|
||||||
|
fun newEntryEntry(
|
||||||
|
key: ConfigKey = KEY,
|
||||||
|
value: String = DEFAULT_VALUE
|
||||||
|
) = StringConfigurationEntry(key, value)
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,8 +8,8 @@ import co.electriccoin.zcash.spackle.LazyWithArgument
|
||||||
import kotlinx.collections.immutable.toPersistentList
|
import kotlinx.collections.immutable.toPersistentList
|
||||||
|
|
||||||
object AndroidConfigurationFactory {
|
object AndroidConfigurationFactory {
|
||||||
|
private val instance =
|
||||||
private val instance = LazyWithArgument<Context, ConfigurationProvider> { context ->
|
LazyWithArgument<Context, ConfigurationProvider> { context ->
|
||||||
new(context)
|
new(context)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -17,8 +17,11 @@ object AndroidConfigurationFactory {
|
||||||
|
|
||||||
// Context will be needed for most cloud providers, e.g. to integrate with Firebase or other
|
// Context will be needed for most cloud providers, e.g. to integrate with Firebase or other
|
||||||
// remote configuration providers.
|
// remote configuration providers.
|
||||||
private fun new(@Suppress("UNUSED_PARAMETER") context: Context): ConfigurationProvider {
|
private fun new(
|
||||||
val configurationProviders = buildList<ConfigurationProvider> {
|
@Suppress("UNUSED_PARAMETER") context: Context
|
||||||
|
): ConfigurationProvider {
|
||||||
|
val configurationProviders =
|
||||||
|
buildList<ConfigurationProvider> {
|
||||||
// For ordering, ensure the IntentConfigurationProvider is first so that it can
|
// For ordering, ensure the IntentConfigurationProvider is first so that it can
|
||||||
// override any other configuration providers.
|
// override any other configuration providers.
|
||||||
if (BuildConfig.DEBUG) {
|
if (BuildConfig.DEBUG) {
|
||||||
|
|
|
@ -8,7 +8,6 @@ import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
|
||||||
internal object IntentConfigurationProvider : ConfigurationProvider {
|
internal object IntentConfigurationProvider : ConfigurationProvider {
|
||||||
|
|
||||||
private val configurationStateFlow = MutableStateFlow(StringConfiguration(persistentMapOf(), null))
|
private val configurationStateFlow = MutableStateFlow(StringConfiguration(persistentMapOf(), null))
|
||||||
|
|
||||||
override fun peekConfiguration() = configurationStateFlow.value
|
override fun peekConfiguration() = configurationStateFlow.value
|
||||||
|
|
|
@ -8,14 +8,18 @@ import kotlinx.collections.immutable.toPersistentMap
|
||||||
import kotlinx.datetime.Clock
|
import kotlinx.datetime.Clock
|
||||||
|
|
||||||
class IntentConfigurationReceiver : BroadcastReceiver() {
|
class IntentConfigurationReceiver : BroadcastReceiver() {
|
||||||
override fun onReceive(context: Context?, intent: Intent?) {
|
override fun onReceive(
|
||||||
|
context: Context?,
|
||||||
|
intent: Intent?
|
||||||
|
) {
|
||||||
intent?.defuse()?.let {
|
intent?.defuse()?.let {
|
||||||
val key = it.getStringExtra(ConfigurationIntent.EXTRA_STRING_KEY)
|
val key = it.getStringExtra(ConfigurationIntent.EXTRA_STRING_KEY)
|
||||||
val value = it.getStringExtra(ConfigurationIntent.EXTRA_STRING_VALUE)
|
val value = it.getStringExtra(ConfigurationIntent.EXTRA_STRING_VALUE)
|
||||||
|
|
||||||
if (null != key) {
|
if (null != key) {
|
||||||
val existingConfiguration = IntentConfigurationProvider.peekConfiguration().configurationMapping
|
val existingConfiguration = IntentConfigurationProvider.peekConfiguration().configurationMapping
|
||||||
val newConfiguration = if (null == value) {
|
val newConfiguration =
|
||||||
|
if (null == value) {
|
||||||
existingConfiguration.remove(key)
|
existingConfiguration.remove(key)
|
||||||
} else {
|
} else {
|
||||||
existingConfiguration + (key to value)
|
existingConfiguration + (key to value)
|
||||||
|
@ -34,7 +38,9 @@ private fun Intent.defuse(): Intent? {
|
||||||
return try {
|
return try {
|
||||||
extras?.containsKey(null)
|
extras?.containsKey(null)
|
||||||
this
|
this
|
||||||
} catch (@Suppress("SwallowedException", "TooGenericExceptionCaught") e: Exception) {
|
} catch (
|
||||||
|
@Suppress("SwallowedException", "TooGenericExceptionCaught") e: Exception
|
||||||
|
) {
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,7 +9,6 @@ import java.util.concurrent.CountDownLatch
|
||||||
import java.util.concurrent.atomic.AtomicBoolean
|
import java.util.concurrent.atomic.AtomicBoolean
|
||||||
|
|
||||||
class AndroidUncaughtExceptionHandlerTest {
|
class AndroidUncaughtExceptionHandlerTest {
|
||||||
|
|
||||||
@Test(expected = IllegalStateException::class)
|
@Test(expected = IllegalStateException::class)
|
||||||
fun requires_main_thread() {
|
fun requires_main_thread() {
|
||||||
AndroidUncaughtExceptionHandler.register(ApplicationProvider.getApplicationContext())
|
AndroidUncaughtExceptionHandler.register(ApplicationProvider.getApplicationContext())
|
||||||
|
|
|
@ -12,7 +12,6 @@ import org.junit.Assert.assertTrue
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
|
||||||
class Components {
|
class Components {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@SmallTest
|
@SmallTest
|
||||||
fun process_names() {
|
fun process_names() {
|
||||||
|
@ -26,16 +25,18 @@ class Components {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun PackageManager.getProviderInfoCompat(componentName: ComponentName) = if (AndroidApiVersion.isAtLeastT) {
|
private fun PackageManager.getProviderInfoCompat(componentName: ComponentName) =
|
||||||
|
if (AndroidApiVersion.isAtLeastT) {
|
||||||
getProviderInfo(componentName, PackageManager.ComponentInfoFlags.of(0))
|
getProviderInfo(componentName, PackageManager.ComponentInfoFlags.of(0))
|
||||||
} else {
|
} else {
|
||||||
@Suppress("Deprecation")
|
@Suppress("Deprecation")
|
||||||
getProviderInfo(componentName, 0)
|
getProviderInfo(componentName, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun PackageManager.getReceiverInfoCompat(componentName: ComponentName) = if (AndroidApiVersion.isAtLeastT) {
|
private fun PackageManager.getReceiverInfoCompat(componentName: ComponentName) =
|
||||||
|
if (AndroidApiVersion.isAtLeastT) {
|
||||||
getReceiverInfo(componentName, PackageManager.ComponentInfoFlags.of(0))
|
getReceiverInfo(componentName, PackageManager.ComponentInfoFlags.of(0))
|
||||||
} else {
|
} else {
|
||||||
@Suppress("Deprecation")
|
@Suppress("Deprecation")
|
||||||
getReceiverInfo(componentName, 0)
|
getReceiverInfo(componentName, 0)
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,7 +6,6 @@ import org.junit.Assert.assertEquals
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
|
||||||
class ReportableExceptionTest {
|
class ReportableExceptionTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun bundle() {
|
fun bundle() {
|
||||||
val reportableException = ReportableExceptionFixture.new()
|
val reportableException = ReportableExceptionFixture.new()
|
||||||
|
|
|
@ -9,7 +9,8 @@ import java.io.File
|
||||||
|
|
||||||
@Suppress("ReturnCount")
|
@Suppress("ReturnCount")
|
||||||
suspend fun ExceptionPath.getExceptionDirectory(context: Context): File? {
|
suspend fun ExceptionPath.getExceptionDirectory(context: Context): File? {
|
||||||
val exceptionDirectory = context.getExternalFilesDirSuspend(null)
|
val exceptionDirectory =
|
||||||
|
context.getExternalFilesDirSuspend(null)
|
||||||
?.let { File(File(it, ExceptionPath.LOG_DIRECTORY_NAME), ExceptionPath.EXCEPTION_DIRECTORY_NAME) }
|
?.let { File(File(it, ExceptionPath.LOG_DIRECTORY_NAME), ExceptionPath.EXCEPTION_DIRECTORY_NAME) }
|
||||||
|
|
||||||
if (null == exceptionDirectory) {
|
if (null == exceptionDirectory) {
|
||||||
|
@ -27,8 +28,12 @@ suspend fun ExceptionPath.getExceptionDirectory(context: Context): File? {
|
||||||
return exceptionDirectory
|
return exceptionDirectory
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun ExceptionPath.getExceptionPath(context: Context, exception: ReportableException): File? {
|
suspend fun ExceptionPath.getExceptionPath(
|
||||||
val exceptionDirectory = getExceptionDirectory(context)
|
context: Context,
|
||||||
|
exception: ReportableException
|
||||||
|
): File? {
|
||||||
|
val exceptionDirectory =
|
||||||
|
getExceptionDirectory(context)
|
||||||
?: return null
|
?: return null
|
||||||
|
|
||||||
return File(exceptionDirectory, newExceptionFileName(exception))
|
return File(exceptionDirectory, newExceptionFileName(exception))
|
||||||
|
|
|
@ -11,7 +11,6 @@ import co.electriccoin.zcash.spackle.process.ProcessNameCompat
|
||||||
import java.util.Collections
|
import java.util.Collections
|
||||||
|
|
||||||
object GlobalCrashReporter {
|
object GlobalCrashReporter {
|
||||||
|
|
||||||
internal const val CRASH_PROCESS_NAME_SUFFIX = ":crash" // $NON-NLS
|
internal const val CRASH_PROCESS_NAME_SUFFIX = ":crash" // $NON-NLS
|
||||||
|
|
||||||
private val intrinsicLock = Any()
|
private val intrinsicLock = Any()
|
||||||
|
@ -33,7 +32,8 @@ object GlobalCrashReporter {
|
||||||
|
|
||||||
synchronized(intrinsicLock) {
|
synchronized(intrinsicLock) {
|
||||||
if (registeredCrashReporters == null) {
|
if (registeredCrashReporters == null) {
|
||||||
registeredCrashReporters = Collections.synchronizedList(
|
registeredCrashReporters =
|
||||||
|
Collections.synchronizedList(
|
||||||
// To prevent a race condition, register the LocalCrashReporter first.
|
// To prevent a race condition, register the LocalCrashReporter first.
|
||||||
// FirebaseCrashReporter does some asynchronous registration internally, while
|
// FirebaseCrashReporter does some asynchronous registration internally, while
|
||||||
// LocalCrashReporter uses AndroidUncaughtExceptionHandler which needs to read
|
// LocalCrashReporter uses AndroidUncaughtExceptionHandler which needs to read
|
||||||
|
|
|
@ -3,7 +3,6 @@ package co.electriccoin.zcash.crash.android.internal
|
||||||
import androidx.annotation.AnyThread
|
import androidx.annotation.AnyThread
|
||||||
|
|
||||||
interface CrashReporter {
|
interface CrashReporter {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Report a caught exception, e.g. within a try-catch.
|
* Report a caught exception, e.g. within a try-catch.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -30,9 +30,10 @@ object FirebaseAppCache {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun getFirebaseAppContainer(context: Context): FirebaseAppContainer = withContext(Dispatchers.IO) {
|
private suspend fun getFirebaseAppContainer(context: Context): FirebaseAppContainer =
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
val firebaseApp = FirebaseApp.initializeApp(context)
|
val firebaseApp = FirebaseApp.initializeApp(context)
|
||||||
FirebaseAppContainer(firebaseApp)
|
FirebaseAppContainer(firebaseApp)
|
||||||
}
|
}
|
||||||
|
|
||||||
private class FirebaseAppContainer(val firebaseApp: FirebaseApp?)
|
private class FirebaseAppContainer(val firebaseApp: FirebaseApp?)
|
||||||
|
|
|
@ -25,11 +25,11 @@ import kotlinx.coroutines.async
|
||||||
internal class FirebaseCrashReporter(
|
internal class FirebaseCrashReporter(
|
||||||
context: Context
|
context: Context
|
||||||
) : CrashReporter {
|
) : CrashReporter {
|
||||||
|
|
||||||
@OptIn(kotlinx.coroutines.DelicateCoroutinesApi::class)
|
@OptIn(kotlinx.coroutines.DelicateCoroutinesApi::class)
|
||||||
private val analyticsScope = CoroutineScope(SupervisorJob() + Dispatchers.Main)
|
private val analyticsScope = CoroutineScope(SupervisorJob() + Dispatchers.Main)
|
||||||
|
|
||||||
private val initFirebaseJob: Deferred<CrashReporter?> = analyticsScope.async {
|
private val initFirebaseJob: Deferred<CrashReporter?> =
|
||||||
|
analyticsScope.async {
|
||||||
FirebaseCrashReporterImpl.getInstance(context)
|
FirebaseCrashReporterImpl.getInstance(context)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -67,7 +67,6 @@ private class FirebaseCrashReporterImpl(
|
||||||
private val firebaseCrashlytics: FirebaseCrashlytics,
|
private val firebaseCrashlytics: FirebaseCrashlytics,
|
||||||
private val firebaseInstallations: FirebaseInstallations
|
private val firebaseInstallations: FirebaseInstallations
|
||||||
) : CrashReporter {
|
) : CrashReporter {
|
||||||
|
|
||||||
@AnyThread
|
@AnyThread
|
||||||
override fun reportCaughtException(exception: Throwable) {
|
override fun reportCaughtException(exception: Throwable) {
|
||||||
firebaseCrashlytics.recordException(exception)
|
firebaseCrashlytics.recordException(exception)
|
||||||
|
@ -90,7 +89,8 @@ private class FirebaseCrashReporterImpl(
|
||||||
* early crashes may be missed. This is a tradeoff we are willing to make in order to avoid
|
* early crashes may be missed. This is a tradeoff we are willing to make in order to avoid
|
||||||
* ANRs.
|
* ANRs.
|
||||||
*/
|
*/
|
||||||
private val lazyWithArgument = SuspendingLazy<Context, CrashReporter?> {
|
private val lazyWithArgument =
|
||||||
|
SuspendingLazy<Context, CrashReporter?> {
|
||||||
if (it.resources.getBoolean(R.bool.co_electriccoin_zcash_crash_is_firebase_enabled)) {
|
if (it.resources.getBoolean(R.bool.co_electriccoin_zcash_crash_is_firebase_enabled)) {
|
||||||
|
|
||||||
// Workaround for disk IO on main thread in Firebase initialization
|
// Workaround for disk IO on main thread in Firebase initialization
|
||||||
|
@ -101,7 +101,8 @@ private class FirebaseCrashReporterImpl(
|
||||||
}
|
}
|
||||||
|
|
||||||
val firebaseInstallations = FirebaseInstallations.getInstance(firebaseApp)
|
val firebaseInstallations = FirebaseInstallations.getInstance(firebaseApp)
|
||||||
val firebaseCrashlytics = FirebaseCrashlytics.getInstance().apply {
|
val firebaseCrashlytics =
|
||||||
|
FirebaseCrashlytics.getInstance().apply {
|
||||||
setCustomKey(
|
setCustomKey(
|
||||||
CrashlyticsUserProperties.IS_TEST,
|
CrashlyticsUserProperties.IS_TEST,
|
||||||
EmulatorWtfUtil.isEmulatorWtf(it) || FirebaseTestLabUtil.isFirebaseTestLab(it)
|
EmulatorWtfUtil.isEmulatorWtf(it) || FirebaseTestLabUtil.isFirebaseTestLab(it)
|
||||||
|
|
|
@ -8,8 +8,12 @@ import co.electriccoin.zcash.crash.android.getExceptionPath
|
||||||
import co.electriccoin.zcash.crash.write
|
import co.electriccoin.zcash.crash.write
|
||||||
|
|
||||||
internal object AndroidExceptionReporter {
|
internal object AndroidExceptionReporter {
|
||||||
internal suspend fun reportException(context: Context, reportableException: ReportableException) {
|
internal suspend fun reportException(
|
||||||
val exceptionPath = ExceptionPath.getExceptionPath(context, reportableException)
|
context: Context,
|
||||||
|
reportableException: ReportableException
|
||||||
|
) {
|
||||||
|
val exceptionPath =
|
||||||
|
ExceptionPath.getExceptionPath(context, reportableException)
|
||||||
?: return
|
?: return
|
||||||
|
|
||||||
reportableException.write(exceptionPath)
|
reportableException.write(exceptionPath)
|
||||||
|
|
|
@ -13,7 +13,8 @@ internal fun ReportableException.Companion.new(
|
||||||
isUncaught: Boolean,
|
isUncaught: Boolean,
|
||||||
clock: Clock = Clock.System
|
clock: Clock = Clock.System
|
||||||
): ReportableException {
|
): ReportableException {
|
||||||
val versionName = context.packageManager.getPackageInfoCompat(context.packageName, 0L).versionName
|
val versionName =
|
||||||
|
context.packageManager.getPackageInfoCompat(context.packageName, 0L).versionName
|
||||||
?: "null"
|
?: "null"
|
||||||
|
|
||||||
return ReportableException(
|
return ReportableException(
|
||||||
|
@ -25,7 +26,8 @@ internal fun ReportableException.Companion.new(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
internal fun ReportableException.toBundle() = Bundle().apply {
|
internal fun ReportableException.toBundle() =
|
||||||
|
Bundle().apply {
|
||||||
// Although Exception is Serializable, some Kotlin Coroutines exception classes break this
|
// Although Exception is Serializable, some Kotlin Coroutines exception classes break this
|
||||||
// API contract. Therefore we have to convert to a string here.
|
// API contract. Therefore we have to convert to a string here.
|
||||||
putSerializable(ReportableException.EXTRA_STRING_CLASS_NAME, exceptionClass)
|
putSerializable(ReportableException.EXTRA_STRING_CLASS_NAME, exceptionClass)
|
||||||
|
@ -33,7 +35,7 @@ internal fun ReportableException.toBundle() = Bundle().apply {
|
||||||
putString(ReportableException.EXTRA_STRING_APP_VERSION, appVersion)
|
putString(ReportableException.EXTRA_STRING_APP_VERSION, appVersion)
|
||||||
putBoolean(ReportableException.EXTRA_BOOLEAN_IS_UNCAUGHT, isUncaught)
|
putBoolean(ReportableException.EXTRA_BOOLEAN_IS_UNCAUGHT, isUncaught)
|
||||||
putLong(ReportableException.EXTRA_LONG_WALLTIME_MILLIS, time.toEpochMilliseconds())
|
putLong(ReportableException.EXTRA_LONG_WALLTIME_MILLIS, time.toEpochMilliseconds())
|
||||||
}
|
}
|
||||||
|
|
||||||
internal fun ReportableException.Companion.fromBundle(bundle: Bundle): ReportableException {
|
internal fun ReportableException.Companion.fromBundle(bundle: Bundle): ReportableException {
|
||||||
val className = bundle.getString(EXTRA_STRING_CLASS_NAME)!!
|
val className = bundle.getString(EXTRA_STRING_CLASS_NAME)!!
|
||||||
|
|
|
@ -12,13 +12,16 @@ internal class AndroidUncaughtExceptionHandler(
|
||||||
context: Context,
|
context: Context,
|
||||||
private val defaultUncaughtExceptionHandler: Thread.UncaughtExceptionHandler
|
private val defaultUncaughtExceptionHandler: Thread.UncaughtExceptionHandler
|
||||||
) : Thread.UncaughtExceptionHandler {
|
) : Thread.UncaughtExceptionHandler {
|
||||||
|
|
||||||
private val applicationContext = context.applicationContext
|
private val applicationContext = context.applicationContext
|
||||||
|
|
||||||
override fun uncaughtException(t: Thread, e: Throwable) {
|
override fun uncaughtException(
|
||||||
|
t: Thread,
|
||||||
|
e: Throwable
|
||||||
|
) {
|
||||||
val reportableException = ReportableException.new(applicationContext, e, true)
|
val reportableException = ReportableException.new(applicationContext, e, true)
|
||||||
|
|
||||||
val isUseSecondaryProcess = applicationContext.resources
|
val isUseSecondaryProcess =
|
||||||
|
applicationContext.resources
|
||||||
.getBoolean(R.bool.co_electriccoin_zcash_crash_is_use_secondary_process)
|
.getBoolean(R.bool.co_electriccoin_zcash_crash_is_use_secondary_process)
|
||||||
|
|
||||||
if (isUseSecondaryProcess) {
|
if (isUseSecondaryProcess) {
|
||||||
|
@ -31,7 +34,6 @@ internal class AndroidUncaughtExceptionHandler(
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
private val isInitialized = AtomicBoolean(false)
|
private val isInitialized = AtomicBoolean(false)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -8,16 +8,18 @@ import kotlinx.coroutines.GlobalScope
|
||||||
|
|
||||||
@OptIn(kotlinx.coroutines.DelicateCoroutinesApi::class)
|
@OptIn(kotlinx.coroutines.DelicateCoroutinesApi::class)
|
||||||
class ExceptionReceiver : CoroutineBroadcastReceiver(GlobalScope) {
|
class ExceptionReceiver : CoroutineBroadcastReceiver(GlobalScope) {
|
||||||
|
override suspend fun onReceiveSuspend(
|
||||||
override suspend fun onReceiveSuspend(context: Context, intent: Intent) {
|
context: Context,
|
||||||
val reportableException = intent.extras?.let { ReportableException.fromBundle(it) }
|
intent: Intent
|
||||||
|
) {
|
||||||
|
val reportableException =
|
||||||
|
intent.extras?.let { ReportableException.fromBundle(it) }
|
||||||
?: return
|
?: return
|
||||||
|
|
||||||
AndroidExceptionReporter.reportException(context, reportableException)
|
AndroidExceptionReporter.reportException(context, reportableException)
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return Explicit intent to broadcast to log the exception.
|
* @return Explicit intent to broadcast to log the exception.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -14,7 +14,6 @@ import kotlinx.coroutines.launch
|
||||||
* Registers an exception handler to write exceptions to disk.
|
* Registers an exception handler to write exceptions to disk.
|
||||||
*/
|
*/
|
||||||
internal class LocalCrashReporter(private val applicationContext: Context) : CrashReporter {
|
internal class LocalCrashReporter(private val applicationContext: Context) : CrashReporter {
|
||||||
|
|
||||||
private val crashReportingScope = CoroutineScope(SupervisorJob() + Dispatchers.Main)
|
private val crashReportingScope = CoroutineScope(SupervisorJob() + Dispatchers.Main)
|
||||||
|
|
||||||
@AnyThread
|
@AnyThread
|
||||||
|
@ -36,7 +35,8 @@ internal class LocalCrashReporter(private val applicationContext: Context) : Cra
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private val lazyWithArgument = LazyWithArgument<Context, CrashReporter> {
|
private val lazyWithArgument =
|
||||||
|
LazyWithArgument<Context, CrashReporter> {
|
||||||
AndroidUncaughtExceptionHandler.register(it)
|
AndroidUncaughtExceptionHandler.register(it)
|
||||||
LocalCrashReporter(it.applicationContext)
|
LocalCrashReporter(it.applicationContext)
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,6 +9,5 @@ data class ReportableException(
|
||||||
val isUncaught: Boolean,
|
val isUncaught: Boolean,
|
||||||
val time: Instant
|
val time: Instant
|
||||||
) {
|
) {
|
||||||
|
|
||||||
companion object
|
companion object
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,8 +14,10 @@ object ExceptionPath {
|
||||||
const val TYPE = "txt"
|
const val TYPE = "txt"
|
||||||
|
|
||||||
@Suppress("MaxLineLength")
|
@Suppress("MaxLineLength")
|
||||||
fun newExceptionFileName(exception: ReportableException, uuid: UUID = UUID.randomUUID()) =
|
fun newExceptionFileName(
|
||||||
"${exception.time.epochSeconds}$SEPARATOR$uuid$SEPARATOR${exception.exceptionClass}$SEPARATOR${exception.isUncaught}.$TYPE"
|
exception: ReportableException,
|
||||||
|
uuid: UUID = UUID.randomUUID()
|
||||||
|
) = "${exception.time.epochSeconds}$SEPARATOR$uuid$SEPARATOR${exception.exceptionClass}$SEPARATOR${exception.isUncaught}.$TYPE"
|
||||||
|
|
||||||
// The exceptions are really just for debugging
|
// The exceptions are really just for debugging
|
||||||
@Suppress("ThrowsCount")
|
@Suppress("ThrowsCount")
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
@file:Suppress("ktlint:filename")
|
@file:Suppress("ktlint:standard:filename")
|
||||||
|
|
||||||
package co.electriccoin.zcash.crash
|
package co.electriccoin.zcash.crash
|
||||||
|
|
||||||
|
@ -8,7 +8,8 @@ import kotlinx.coroutines.withContext
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
suspend fun ReportableException.write(path: File) {
|
suspend fun ReportableException.write(path: File) {
|
||||||
val exceptionString = buildString {
|
val exceptionString =
|
||||||
|
buildString {
|
||||||
appendLine("App version: $appVersion")
|
appendLine("App version: $appVersion")
|
||||||
appendLine("Is uncaught: $isUncaught")
|
appendLine("Is uncaught: $isUncaught")
|
||||||
appendLine("Time: $time")
|
appendLine("Time: $time")
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
@file:Suppress("ktlint:filename")
|
@file:Suppress("ktlint:standard:filename")
|
||||||
|
|
||||||
package co.electriccoin.zcash.crash
|
package co.electriccoin.zcash.crash
|
||||||
|
|
||||||
|
|
|
@ -143,7 +143,7 @@ FULLADLE_VERSION=0.17.4
|
||||||
GOOGLE_PLAY_SERVICES_GRADLE_PLUGIN_VERSION=4.3.15
|
GOOGLE_PLAY_SERVICES_GRADLE_PLUGIN_VERSION=4.3.15
|
||||||
GRADLE_VERSIONS_PLUGIN_VERSION=0.47.0
|
GRADLE_VERSIONS_PLUGIN_VERSION=0.47.0
|
||||||
JGIT_VERSION=6.4.0.202211300538-r
|
JGIT_VERSION=6.4.0.202211300538-r
|
||||||
KTLINT_VERSION=0.49.0
|
KTLINT_VERSION=1.0.1
|
||||||
|
|
||||||
ACCOMPANIST_PERMISSIONS_VERSION=0.32.0
|
ACCOMPANIST_PERMISSIONS_VERSION=0.32.0
|
||||||
ANDROIDX_ACTIVITY_VERSION=1.8.1
|
ANDROIDX_ACTIVITY_VERSION=1.8.1
|
||||||
|
|
|
@ -4,10 +4,12 @@ import co.electriccoin.zcash.preference.model.entry.PreferenceKey
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
|
||||||
interface PreferenceProvider {
|
interface PreferenceProvider {
|
||||||
|
|
||||||
suspend fun hasKey(key: PreferenceKey): Boolean
|
suspend fun hasKey(key: PreferenceKey): Boolean
|
||||||
|
|
||||||
suspend fun putString(key: PreferenceKey, value: String?)
|
suspend fun putString(
|
||||||
|
key: PreferenceKey,
|
||||||
|
value: String?
|
||||||
|
)
|
||||||
|
|
||||||
suspend fun getString(key: PreferenceKey): String?
|
suspend fun getString(key: PreferenceKey): String?
|
||||||
|
|
||||||
|
|
|
@ -6,9 +6,9 @@ data class BooleanPreferenceDefault(
|
||||||
override val key: PreferenceKey,
|
override val key: PreferenceKey,
|
||||||
private val defaultValue: Boolean
|
private val defaultValue: Boolean
|
||||||
) : PreferenceDefault<Boolean> {
|
) : PreferenceDefault<Boolean> {
|
||||||
|
|
||||||
@Suppress("SwallowedException")
|
@Suppress("SwallowedException")
|
||||||
override suspend fun getValue(preferenceProvider: PreferenceProvider) = preferenceProvider.getString(key)?.let {
|
override suspend fun getValue(preferenceProvider: PreferenceProvider) =
|
||||||
|
preferenceProvider.getString(key)?.let {
|
||||||
try {
|
try {
|
||||||
it.toBooleanStrict()
|
it.toBooleanStrict()
|
||||||
} catch (e: IllegalArgumentException) {
|
} catch (e: IllegalArgumentException) {
|
||||||
|
@ -18,7 +18,10 @@ data class BooleanPreferenceDefault(
|
||||||
}
|
}
|
||||||
} ?: defaultValue
|
} ?: defaultValue
|
||||||
|
|
||||||
override suspend fun putValue(preferenceProvider: PreferenceProvider, newValue: Boolean) {
|
override suspend fun putValue(
|
||||||
|
preferenceProvider: PreferenceProvider,
|
||||||
|
newValue: Boolean
|
||||||
|
) {
|
||||||
preferenceProvider.putString(key, newValue.toString())
|
preferenceProvider.putString(key, newValue.toString())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,8 +6,8 @@ data class IntegerPreferenceDefault(
|
||||||
override val key: PreferenceKey,
|
override val key: PreferenceKey,
|
||||||
private val defaultValue: Int
|
private val defaultValue: Int
|
||||||
) : PreferenceDefault<Int> {
|
) : PreferenceDefault<Int> {
|
||||||
|
override suspend fun getValue(preferenceProvider: PreferenceProvider) =
|
||||||
override suspend fun getValue(preferenceProvider: PreferenceProvider) = preferenceProvider.getString(key)?.let {
|
preferenceProvider.getString(key)?.let {
|
||||||
try {
|
try {
|
||||||
it.toInt()
|
it.toInt()
|
||||||
} catch (e: NumberFormatException) {
|
} catch (e: NumberFormatException) {
|
||||||
|
@ -17,7 +17,10 @@ data class IntegerPreferenceDefault(
|
||||||
}
|
}
|
||||||
} ?: defaultValue
|
} ?: defaultValue
|
||||||
|
|
||||||
override suspend fun putValue(preferenceProvider: PreferenceProvider, newValue: Int) {
|
override suspend fun putValue(
|
||||||
|
preferenceProvider: PreferenceProvider,
|
||||||
|
newValue: Int
|
||||||
|
) {
|
||||||
preferenceProvider.putString(key, newValue.toString())
|
preferenceProvider.putString(key, newValue.toString())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,7 +11,8 @@ import kotlinx.coroutines.flow.map
|
||||||
* variation in default value. Clients define the key and default value together, rather than just
|
* variation in default value. Clients define the key and default value together, rather than just
|
||||||
* the key.
|
* the key.
|
||||||
*/
|
*/
|
||||||
/*
|
interface PreferenceDefault<T> {
|
||||||
|
/*
|
||||||
* API note: the default value is not available through the public interface in order to prevent
|
* 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 preference value.
|
* clients from accidentally using the default value instead of the preference value.
|
||||||
*
|
*
|
||||||
|
@ -20,7 +21,6 @@ import kotlinx.coroutines.flow.map
|
||||||
* Overall the number of Integer preference entries is expected to be low compared to Booleans,
|
* Overall the number of Integer preference entries is expected to be low compared to Booleans,
|
||||||
* and perhaps many Integer values will also fit within the autoboxing cache.
|
* and perhaps many Integer values will also fit within the autoboxing cache.
|
||||||
*/
|
*/
|
||||||
interface PreferenceDefault<T> {
|
|
||||||
|
|
||||||
val key: PreferenceKey
|
val key: PreferenceKey
|
||||||
|
|
||||||
|
@ -34,14 +34,18 @@ interface PreferenceDefault<T> {
|
||||||
* @param preferenceProvider Provides actual preference values.
|
* @param preferenceProvider Provides actual preference values.
|
||||||
* @param newValue New value to write.
|
* @param newValue New value to write.
|
||||||
*/
|
*/
|
||||||
suspend fun putValue(preferenceProvider: PreferenceProvider, newValue: T)
|
suspend fun putValue(
|
||||||
|
preferenceProvider: PreferenceProvider,
|
||||||
|
newValue: T
|
||||||
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param preferenceProvider Provides actual preference values.
|
* @param preferenceProvider Provides actual preference values.
|
||||||
* @return Flow that emits preference changes. Note that implementations should emit an initial value
|
* @return Flow that emits preference changes. Note that implementations should emit an initial value
|
||||||
* indicating what was stored in the preferences, in addition to subsequent updates.
|
* indicating what was stored in the preferences, in addition to subsequent updates.
|
||||||
*/
|
*/
|
||||||
fun observe(preferenceProvider: PreferenceProvider): Flow<T> = preferenceProvider.observe(key)
|
fun observe(preferenceProvider: PreferenceProvider): Flow<T> =
|
||||||
|
preferenceProvider.observe(key)
|
||||||
.map { getValue(preferenceProvider) }
|
.map { getValue(preferenceProvider) }
|
||||||
.distinctUntilChanged()
|
.distinctUntilChanged()
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,11 +6,14 @@ data class StringPreferenceDefault(
|
||||||
override val key: PreferenceKey,
|
override val key: PreferenceKey,
|
||||||
private val defaultValue: String
|
private val defaultValue: String
|
||||||
) : PreferenceDefault<String> {
|
) : PreferenceDefault<String> {
|
||||||
|
override suspend fun getValue(preferenceProvider: PreferenceProvider) =
|
||||||
override suspend fun getValue(preferenceProvider: PreferenceProvider) = preferenceProvider.getString(key)
|
preferenceProvider.getString(key)
|
||||||
?: defaultValue
|
?: defaultValue
|
||||||
|
|
||||||
override suspend fun putValue(preferenceProvider: PreferenceProvider, newValue: String) {
|
override suspend fun putValue(
|
||||||
|
preferenceProvider: PreferenceProvider,
|
||||||
|
newValue: String
|
||||||
|
) {
|
||||||
preferenceProvider.putString(key, newValue)
|
preferenceProvider.putString(key, newValue)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,30 +16,36 @@ class BooleanPreferenceDefaultTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun value_default_true() = runTest {
|
fun value_default_true() =
|
||||||
|
runTest {
|
||||||
val entry = BooleanPreferenceDefaultFixture.newTrue()
|
val entry = BooleanPreferenceDefaultFixture.newTrue()
|
||||||
assertTrue(entry.getValue(MockPreferenceProvider()))
|
assertTrue(entry.getValue(MockPreferenceProvider()))
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun value_default_false() = runTest {
|
fun value_default_false() =
|
||||||
|
runTest {
|
||||||
val entry = BooleanPreferenceDefaultFixture.newFalse()
|
val entry = BooleanPreferenceDefaultFixture.newFalse()
|
||||||
assertFalse(entry.getValue(MockPreferenceProvider()))
|
assertFalse(entry.getValue(MockPreferenceProvider()))
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun value_from_config_false() = runTest {
|
fun value_from_config_false() =
|
||||||
|
runTest {
|
||||||
val entry = BooleanPreferenceDefaultFixture.newTrue()
|
val entry = BooleanPreferenceDefaultFixture.newTrue()
|
||||||
val mockPreferenceProvider = MockPreferenceProvider {
|
val mockPreferenceProvider =
|
||||||
|
MockPreferenceProvider {
|
||||||
mutableMapOf(BooleanPreferenceDefaultFixture.KEY.key to false.toString())
|
mutableMapOf(BooleanPreferenceDefaultFixture.KEY.key to false.toString())
|
||||||
}
|
}
|
||||||
assertFalse(entry.getValue(mockPreferenceProvider))
|
assertFalse(entry.getValue(mockPreferenceProvider))
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun value_from_config_true() = runTest {
|
fun value_from_config_true() =
|
||||||
|
runTest {
|
||||||
val entry = BooleanPreferenceDefaultFixture.newTrue()
|
val entry = BooleanPreferenceDefaultFixture.newTrue()
|
||||||
val mockPreferenceProvider = MockPreferenceProvider {
|
val mockPreferenceProvider =
|
||||||
|
MockPreferenceProvider {
|
||||||
mutableMapOf(BooleanPreferenceDefaultFixture.KEY.key to true.toString())
|
mutableMapOf(BooleanPreferenceDefaultFixture.KEY.key to true.toString())
|
||||||
}
|
}
|
||||||
assertTrue(entry.getValue(mockPreferenceProvider))
|
assertTrue(entry.getValue(mockPreferenceProvider))
|
||||||
|
|
|
@ -15,17 +15,20 @@ class IntegerPreferenceDefaultTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun value_default() = runTest {
|
fun value_default() =
|
||||||
|
runTest {
|
||||||
val entry = IntegerPreferenceDefaultFixture.new()
|
val entry = IntegerPreferenceDefaultFixture.new()
|
||||||
assertEquals(IntegerPreferenceDefaultFixture.DEFAULT_VALUE, entry.getValue(MockPreferenceProvider()))
|
assertEquals(IntegerPreferenceDefaultFixture.DEFAULT_VALUE, entry.getValue(MockPreferenceProvider()))
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun value_override() = runTest {
|
fun value_override() =
|
||||||
|
runTest {
|
||||||
val expected = IntegerPreferenceDefaultFixture.DEFAULT_VALUE + 5
|
val expected = IntegerPreferenceDefaultFixture.DEFAULT_VALUE + 5
|
||||||
|
|
||||||
val entry = IntegerPreferenceDefaultFixture.new()
|
val entry = IntegerPreferenceDefaultFixture.new()
|
||||||
val mockPreferenceProvider = MockPreferenceProvider {
|
val mockPreferenceProvider =
|
||||||
|
MockPreferenceProvider {
|
||||||
mutableMapOf(StringDefaultPreferenceFixture.KEY.key to expected.toString())
|
mutableMapOf(StringDefaultPreferenceFixture.KEY.key to expected.toString())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -14,16 +14,19 @@ class StringPreferenceDefaultTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun value_default() = runTest {
|
fun value_default() =
|
||||||
|
runTest {
|
||||||
val entry = StringDefaultPreferenceFixture.new()
|
val entry = StringDefaultPreferenceFixture.new()
|
||||||
assertEquals(StringDefaultPreferenceFixture.DEFAULT_VALUE, entry.getValue(MockPreferenceProvider()))
|
assertEquals(StringDefaultPreferenceFixture.DEFAULT_VALUE, entry.getValue(MockPreferenceProvider()))
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun value_override() = runTest {
|
fun value_override() =
|
||||||
|
runTest {
|
||||||
val entry = StringDefaultPreferenceFixture.new()
|
val entry = StringDefaultPreferenceFixture.new()
|
||||||
|
|
||||||
val mockPreferenceProvider = MockPreferenceProvider {
|
val mockPreferenceProvider =
|
||||||
|
MockPreferenceProvider {
|
||||||
mutableMapOf(StringDefaultPreferenceFixture.KEY.key to "override")
|
mutableMapOf(StringDefaultPreferenceFixture.KEY.key to "override")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -11,7 +11,6 @@ import kotlinx.coroutines.flow.flow
|
||||||
class MockPreferenceProvider(
|
class MockPreferenceProvider(
|
||||||
mutableMapFactory: () -> MutableMap<String, String?> = { mutableMapOf() }
|
mutableMapFactory: () -> MutableMap<String, String?> = { mutableMapOf() }
|
||||||
) : PreferenceProvider {
|
) : PreferenceProvider {
|
||||||
|
|
||||||
private val map = mutableMapFactory()
|
private val map = mutableMapFactory()
|
||||||
|
|
||||||
override suspend fun getString(key: PreferenceKey) = map[key.key]
|
override suspend fun getString(key: PreferenceKey) = map[key.key]
|
||||||
|
@ -21,7 +20,10 @@ class MockPreferenceProvider(
|
||||||
|
|
||||||
override suspend fun hasKey(key: PreferenceKey) = map.containsKey(key.key)
|
override suspend fun hasKey(key: PreferenceKey) = map.containsKey(key.key)
|
||||||
|
|
||||||
override suspend fun putString(key: PreferenceKey, value: String?) {
|
override suspend fun putString(
|
||||||
|
key: PreferenceKey,
|
||||||
|
value: String?
|
||||||
|
) {
|
||||||
map[key.key] = value
|
map[key.key] = value
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,6 +5,8 @@ import co.electriccoin.zcash.preference.model.entry.PreferenceKey
|
||||||
|
|
||||||
object BooleanPreferenceDefaultFixture {
|
object BooleanPreferenceDefaultFixture {
|
||||||
val KEY = PreferenceKey("some_boolean_key") // $NON-NLS
|
val KEY = PreferenceKey("some_boolean_key") // $NON-NLS
|
||||||
|
|
||||||
fun newTrue() = BooleanPreferenceDefault(KEY, true)
|
fun newTrue() = BooleanPreferenceDefault(KEY, true)
|
||||||
|
|
||||||
fun newFalse() = BooleanPreferenceDefault(KEY, false)
|
fun newFalse() = BooleanPreferenceDefault(KEY, false)
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,6 +6,9 @@ import co.electriccoin.zcash.preference.model.entry.PreferenceKey
|
||||||
object IntegerPreferenceDefaultFixture {
|
object IntegerPreferenceDefaultFixture {
|
||||||
val KEY = PreferenceKey("some_string_key") // $NON-NLS
|
val KEY = PreferenceKey("some_string_key") // $NON-NLS
|
||||||
const val DEFAULT_VALUE = 123
|
const val DEFAULT_VALUE = 123
|
||||||
fun new(preferenceKey: PreferenceKey = KEY, value: Int = DEFAULT_VALUE) =
|
|
||||||
IntegerPreferenceDefault(preferenceKey, value)
|
fun new(
|
||||||
|
preferenceKey: PreferenceKey = KEY,
|
||||||
|
value: Int = DEFAULT_VALUE
|
||||||
|
) = IntegerPreferenceDefault(preferenceKey, value)
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,6 +6,9 @@ import co.electriccoin.zcash.preference.model.entry.StringPreferenceDefault
|
||||||
object StringDefaultPreferenceFixture {
|
object StringDefaultPreferenceFixture {
|
||||||
val KEY = PreferenceKey("some_string_key") // $NON-NLS
|
val KEY = PreferenceKey("some_string_key") // $NON-NLS
|
||||||
const val DEFAULT_VALUE = "some_default_value" // $NON-NLS
|
const val DEFAULT_VALUE = "some_default_value" // $NON-NLS
|
||||||
fun new(preferenceKey: PreferenceKey = KEY, value: String = DEFAULT_VALUE) =
|
|
||||||
StringPreferenceDefault(preferenceKey, value)
|
fun new(
|
||||||
|
preferenceKey: PreferenceKey = KEY,
|
||||||
|
value: String = DEFAULT_VALUE
|
||||||
|
) = StringPreferenceDefault(preferenceKey, value)
|
||||||
}
|
}
|
||||||
|
|
|
@ -35,10 +35,12 @@ class EncryptedPreferenceProviderTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@SmallTest
|
@SmallTest
|
||||||
fun put_and_get_string() = runBlocking {
|
fun put_and_get_string() =
|
||||||
|
runBlocking {
|
||||||
val expectedValue = StringDefaultPreferenceFixture.DEFAULT_VALUE + "extra"
|
val expectedValue = StringDefaultPreferenceFixture.DEFAULT_VALUE + "extra"
|
||||||
|
|
||||||
val preferenceProvider = new().apply {
|
val preferenceProvider =
|
||||||
|
new().apply {
|
||||||
putString(StringDefaultPreferenceFixture.KEY, expectedValue)
|
putString(StringDefaultPreferenceFixture.KEY, expectedValue)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -47,7 +49,8 @@ class EncryptedPreferenceProviderTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@SmallTest
|
@SmallTest
|
||||||
fun hasKey_false() = runBlocking {
|
fun hasKey_false() =
|
||||||
|
runBlocking {
|
||||||
val preferenceProvider = new()
|
val preferenceProvider = new()
|
||||||
|
|
||||||
assertFalse(preferenceProvider.hasKey(StringDefaultPreferenceFixture.new().key))
|
assertFalse(preferenceProvider.hasKey(StringDefaultPreferenceFixture.new().key))
|
||||||
|
@ -55,10 +58,12 @@ class EncryptedPreferenceProviderTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@SmallTest
|
@SmallTest
|
||||||
fun put_and_check_key() = runBlocking {
|
fun put_and_check_key() =
|
||||||
|
runBlocking {
|
||||||
val expectedValue = StringDefaultPreferenceFixture.DEFAULT_VALUE + "extra"
|
val expectedValue = StringDefaultPreferenceFixture.DEFAULT_VALUE + "extra"
|
||||||
|
|
||||||
val preferenceProvider = new().apply {
|
val preferenceProvider =
|
||||||
|
new().apply {
|
||||||
putString(StringDefaultPreferenceFixture.KEY, expectedValue)
|
putString(StringDefaultPreferenceFixture.KEY, expectedValue)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -69,14 +74,16 @@ class EncryptedPreferenceProviderTest {
|
||||||
// e.g. the directory path and the fact the preferences are stored as XML
|
// e.g. the directory path and the fact the preferences are stored as XML
|
||||||
@Test
|
@Test
|
||||||
@SmallTest
|
@SmallTest
|
||||||
fun verify_no_plaintext() = runBlocking {
|
fun verify_no_plaintext() =
|
||||||
|
runBlocking {
|
||||||
val expectedValue = StringDefaultPreferenceFixture.DEFAULT_VALUE + "extra"
|
val expectedValue = StringDefaultPreferenceFixture.DEFAULT_VALUE + "extra"
|
||||||
|
|
||||||
new().apply {
|
new().apply {
|
||||||
putString(StringDefaultPreferenceFixture.KEY, expectedValue)
|
putString(StringDefaultPreferenceFixture.KEY, expectedValue)
|
||||||
}
|
}
|
||||||
|
|
||||||
val text = File(
|
val text =
|
||||||
|
File(
|
||||||
File(ApplicationProvider.getApplicationContext<Context>().dataDir, "shared_prefs"),
|
File(ApplicationProvider.getApplicationContext<Context>().dataDir, "shared_prefs"),
|
||||||
"$FILENAME.xml"
|
"$FILENAME.xml"
|
||||||
).readText()
|
).readText()
|
||||||
|
@ -87,7 +94,9 @@ class EncryptedPreferenceProviderTest {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private val FILENAME = "encrypted_preference_test"
|
private val FILENAME = "encrypted_preference_test"
|
||||||
private suspend fun new() = AndroidPreferenceProvider.newEncrypted(
|
|
||||||
|
private suspend fun new() =
|
||||||
|
AndroidPreferenceProvider.newEncrypted(
|
||||||
ApplicationProvider.getApplicationContext(),
|
ApplicationProvider.getApplicationContext(),
|
||||||
FILENAME
|
FILENAME
|
||||||
)
|
)
|
||||||
|
|
|
@ -33,10 +33,12 @@ class StandardPreferenceProviderTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@SmallTest
|
@SmallTest
|
||||||
fun put_and_get_string() = runBlocking {
|
fun put_and_get_string() =
|
||||||
|
runBlocking {
|
||||||
val expectedValue = StringDefaultPreferenceFixture.DEFAULT_VALUE + "extra"
|
val expectedValue = StringDefaultPreferenceFixture.DEFAULT_VALUE + "extra"
|
||||||
|
|
||||||
val preferenceProvider = new().apply {
|
val preferenceProvider =
|
||||||
|
new().apply {
|
||||||
putString(StringDefaultPreferenceFixture.KEY, expectedValue)
|
putString(StringDefaultPreferenceFixture.KEY, expectedValue)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -45,7 +47,8 @@ class StandardPreferenceProviderTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@SmallTest
|
@SmallTest
|
||||||
fun hasKey_false() = runBlocking {
|
fun hasKey_false() =
|
||||||
|
runBlocking {
|
||||||
val preferenceProvider = new()
|
val preferenceProvider = new()
|
||||||
|
|
||||||
assertFalse(preferenceProvider.hasKey(StringDefaultPreferenceFixture.new().key))
|
assertFalse(preferenceProvider.hasKey(StringDefaultPreferenceFixture.new().key))
|
||||||
|
@ -53,10 +56,12 @@ class StandardPreferenceProviderTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@SmallTest
|
@SmallTest
|
||||||
fun put_and_check_key() = runBlocking {
|
fun put_and_check_key() =
|
||||||
|
runBlocking {
|
||||||
val expectedValue = StringDefaultPreferenceFixture.DEFAULT_VALUE + "extra"
|
val expectedValue = StringDefaultPreferenceFixture.DEFAULT_VALUE + "extra"
|
||||||
|
|
||||||
val preferenceProvider = new().apply {
|
val preferenceProvider =
|
||||||
|
new().apply {
|
||||||
putString(StringDefaultPreferenceFixture.KEY, expectedValue)
|
putString(StringDefaultPreferenceFixture.KEY, expectedValue)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -65,7 +70,9 @@ class StandardPreferenceProviderTest {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private val FILENAME = "encrypted_preference_test"
|
private val FILENAME = "encrypted_preference_test"
|
||||||
private suspend fun new() = AndroidPreferenceProvider.newStandard(
|
|
||||||
|
private suspend fun new() =
|
||||||
|
AndroidPreferenceProvider.newStandard(
|
||||||
ApplicationProvider.getApplicationContext(),
|
ApplicationProvider.getApplicationContext(),
|
||||||
FILENAME
|
FILENAME
|
||||||
)
|
)
|
||||||
|
|
|
@ -6,6 +6,7 @@ import co.electriccoin.zcash.preference.model.entry.StringPreferenceDefault
|
||||||
object StringDefaultPreferenceFixture {
|
object StringDefaultPreferenceFixture {
|
||||||
val KEY = PreferenceKey("some_string_key") // $NON-NLS
|
val KEY = PreferenceKey("some_string_key") // $NON-NLS
|
||||||
const val DEFAULT_VALUE = "some_default_value" // $NON-NLS
|
const val DEFAULT_VALUE = "some_default_value" // $NON-NLS
|
||||||
|
|
||||||
fun new(
|
fun new(
|
||||||
preferenceKey: PreferenceKey = KEY,
|
preferenceKey: PreferenceKey = KEY,
|
||||||
value: String = DEFAULT_VALUE
|
value: String = DEFAULT_VALUE
|
||||||
|
|
|
@ -26,21 +26,25 @@ import java.util.concurrent.Executors
|
||||||
* this instance lives for the lifetime of the application. Constructing multiple instances will
|
* this instance lives for the lifetime of the application. Constructing multiple instances will
|
||||||
* potentially corrupt preference data and will leak resources.
|
* potentially corrupt preference data and will leak resources.
|
||||||
*/
|
*/
|
||||||
/*
|
|
||||||
* Implementation note: EncryptedSharedPreferences are not thread-safe, so this implementation
|
|
||||||
* confines them to a single background thread.
|
|
||||||
*/
|
|
||||||
class AndroidPreferenceProvider(
|
class AndroidPreferenceProvider(
|
||||||
private val sharedPreferences: SharedPreferences,
|
private val sharedPreferences: SharedPreferences,
|
||||||
private val dispatcher: CoroutineDispatcher
|
private val dispatcher: CoroutineDispatcher
|
||||||
) : PreferenceProvider {
|
) : PreferenceProvider {
|
||||||
|
/*
|
||||||
|
* Implementation note: EncryptedSharedPreferences are not thread-safe, so this implementation
|
||||||
|
* confines them to a single background thread.
|
||||||
|
*/
|
||||||
|
|
||||||
override suspend fun hasKey(key: PreferenceKey) = withContext(dispatcher) {
|
override suspend fun hasKey(key: PreferenceKey) =
|
||||||
|
withContext(dispatcher) {
|
||||||
sharedPreferences.contains(key.key)
|
sharedPreferences.contains(key.key)
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("ApplySharedPref")
|
@SuppressLint("ApplySharedPref")
|
||||||
override suspend fun putString(key: PreferenceKey, value: String?) = withContext(dispatcher) {
|
override suspend fun putString(
|
||||||
|
key: PreferenceKey,
|
||||||
|
value: String?
|
||||||
|
) = withContext(dispatcher) {
|
||||||
val editor = sharedPreferences.edit()
|
val editor = sharedPreferences.edit()
|
||||||
|
|
||||||
editor.putString(key.key, value)
|
editor.putString(key.key, value)
|
||||||
|
@ -50,12 +54,15 @@ class AndroidPreferenceProvider(
|
||||||
Unit
|
Unit
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun getString(key: PreferenceKey) = withContext(dispatcher) {
|
override suspend fun getString(key: PreferenceKey) =
|
||||||
|
withContext(dispatcher) {
|
||||||
sharedPreferences.getString(key.key, null)
|
sharedPreferences.getString(key.key, null)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun observe(key: PreferenceKey): Flow<String?> = callbackFlow<Unit> {
|
override fun observe(key: PreferenceKey): Flow<String?> =
|
||||||
val listener = SharedPreferences.OnSharedPreferenceChangeListener { _, _ ->
|
callbackFlow<Unit> {
|
||||||
|
val listener =
|
||||||
|
SharedPreferences.OnSharedPreferenceChangeListener { _, _ ->
|
||||||
// Callback on main thread
|
// Callback on main thread
|
||||||
trySend(Unit)
|
trySend(Unit)
|
||||||
}
|
}
|
||||||
|
@ -71,35 +78,44 @@ class AndroidPreferenceProvider(
|
||||||
.map { getString(key) }
|
.map { getString(key) }
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
suspend fun newStandard(context: Context, filename: String): PreferenceProvider {
|
suspend fun newStandard(
|
||||||
|
context: Context,
|
||||||
|
filename: String
|
||||||
|
): PreferenceProvider {
|
||||||
/*
|
/*
|
||||||
* Because of this line, we don't want multiple instances of this object created
|
* Because of this line, we don't want multiple instances of this object created
|
||||||
* because we don't clean up the thread afterwards.
|
* because we don't clean up the thread afterwards.
|
||||||
*/
|
*/
|
||||||
val singleThreadedDispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher()
|
val singleThreadedDispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher()
|
||||||
|
|
||||||
val sharedPreferences = withContext(singleThreadedDispatcher) {
|
val sharedPreferences =
|
||||||
|
withContext(singleThreadedDispatcher) {
|
||||||
context.getSharedPreferences(filename, Context.MODE_PRIVATE)
|
context.getSharedPreferences(filename, Context.MODE_PRIVATE)
|
||||||
}
|
}
|
||||||
|
|
||||||
return AndroidPreferenceProvider(sharedPreferences, singleThreadedDispatcher)
|
return AndroidPreferenceProvider(sharedPreferences, singleThreadedDispatcher)
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun newEncrypted(context: Context, filename: String): PreferenceProvider {
|
suspend fun newEncrypted(
|
||||||
|
context: Context,
|
||||||
|
filename: String
|
||||||
|
): PreferenceProvider {
|
||||||
/*
|
/*
|
||||||
* Because of this line, we don't want multiple instances of this object created
|
* Because of this line, we don't want multiple instances of this object created
|
||||||
* because we don't clean up the thread afterwards.
|
* because we don't clean up the thread afterwards.
|
||||||
*/
|
*/
|
||||||
val singleThreadedDispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher()
|
val singleThreadedDispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher()
|
||||||
|
|
||||||
val mainKey = withContext(singleThreadedDispatcher) {
|
val mainKey =
|
||||||
|
withContext(singleThreadedDispatcher) {
|
||||||
@Suppress("BlockingMethodInNonBlockingContext")
|
@Suppress("BlockingMethodInNonBlockingContext")
|
||||||
MasterKey.Builder(context).apply {
|
MasterKey.Builder(context).apply {
|
||||||
setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
|
setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
|
||||||
}.build()
|
}.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
val sharedPreferences = withContext(singleThreadedDispatcher) {
|
val sharedPreferences =
|
||||||
|
withContext(singleThreadedDispatcher) {
|
||||||
@Suppress("BlockingMethodInNonBlockingContext")
|
@Suppress("BlockingMethodInNonBlockingContext")
|
||||||
EncryptedSharedPreferences.create(
|
EncryptedSharedPreferences.create(
|
||||||
context,
|
context,
|
||||||
|
|
|
@ -8,10 +8,10 @@ import org.junit.Test
|
||||||
import kotlin.test.assertEquals
|
import kotlin.test.assertEquals
|
||||||
|
|
||||||
class PercentDecimalExtTest {
|
class PercentDecimalExtTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@SmallTest
|
@SmallTest
|
||||||
fun parse_non_zero_percent_decimal_test() = runTest {
|
fun parse_non_zero_percent_decimal_test() =
|
||||||
|
runTest {
|
||||||
val parsed = PercentDecimal(0.1234f).toPercentageWithDecimal()
|
val parsed = PercentDecimal(0.1234f).toPercentageWithDecimal()
|
||||||
|
|
||||||
assertEquals("12${MonetarySeparators.current().decimal}34", parsed)
|
assertEquals("12${MonetarySeparators.current().decimal}34", parsed)
|
||||||
|
@ -19,7 +19,8 @@ class PercentDecimalExtTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@SmallTest
|
@SmallTest
|
||||||
fun parse_zero_percent_decimal_test() = runTest {
|
fun parse_zero_percent_decimal_test() =
|
||||||
|
runTest {
|
||||||
val parsed = PercentDecimal(0.0000f).toPercentageWithDecimal()
|
val parsed = PercentDecimal(0.0000f).toPercentageWithDecimal()
|
||||||
|
|
||||||
assertEquals("0${MonetarySeparators.current().decimal}00", parsed)
|
assertEquals("0${MonetarySeparators.current().decimal}00", parsed)
|
||||||
|
@ -27,7 +28,8 @@ class PercentDecimalExtTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@SmallTest
|
@SmallTest
|
||||||
fun parse_max_percent_decimal_test() = runTest {
|
fun parse_max_percent_decimal_test() =
|
||||||
|
runTest {
|
||||||
val parsed = PercentDecimal(1f).toPercentageWithDecimal()
|
val parsed = PercentDecimal(1f).toPercentageWithDecimal()
|
||||||
|
|
||||||
assertEquals("100${MonetarySeparators.current().decimal}00", parsed)
|
assertEquals("100${MonetarySeparators.current().decimal}00", parsed)
|
||||||
|
@ -35,7 +37,8 @@ class PercentDecimalExtTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@SmallTest
|
@SmallTest
|
||||||
fun parse_min_percent_decimal_test() = runTest {
|
fun parse_min_percent_decimal_test() =
|
||||||
|
runTest {
|
||||||
val parsed = PercentDecimal(0f).toPercentageWithDecimal()
|
val parsed = PercentDecimal(0f).toPercentageWithDecimal()
|
||||||
|
|
||||||
assertEquals("0${MonetarySeparators.current().decimal}00", parsed)
|
assertEquals("0${MonetarySeparators.current().decimal}00", parsed)
|
||||||
|
@ -43,7 +46,8 @@ class PercentDecimalExtTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@SmallTest
|
@SmallTest
|
||||||
fun parse_round_down_percent_decimal_test() = runTest {
|
fun parse_round_down_percent_decimal_test() =
|
||||||
|
runTest {
|
||||||
val parsed = PercentDecimal(0.11111f).toPercentageWithDecimal()
|
val parsed = PercentDecimal(0.11111f).toPercentageWithDecimal()
|
||||||
|
|
||||||
assertEquals("11${MonetarySeparators.current().decimal}11", parsed)
|
assertEquals("11${MonetarySeparators.current().decimal}11", parsed)
|
||||||
|
@ -51,7 +55,8 @@ class PercentDecimalExtTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@SmallTest
|
@SmallTest
|
||||||
fun parse_round_up_percent_decimal_test() = runTest {
|
fun parse_round_up_percent_decimal_test() =
|
||||||
|
runTest {
|
||||||
val parsed = PercentDecimal(0.11119f).toPercentageWithDecimal()
|
val parsed = PercentDecimal(0.11119f).toPercentageWithDecimal()
|
||||||
|
|
||||||
assertEquals("11${MonetarySeparators.current().decimal}12", parsed)
|
assertEquals("11${MonetarySeparators.current().decimal}12", parsed)
|
||||||
|
|
|
@ -15,7 +15,6 @@ import kotlin.test.assertNotNull
|
||||||
import kotlin.test.assertTrue
|
import kotlin.test.assertTrue
|
||||||
|
|
||||||
class ZecRequestTest {
|
class ZecRequestTest {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val URI: String = "zcash:tmXuTnE11JojToagTqxXUn6KvdxDE3iLKbp?amount=1&message=Hello%20world!"
|
private const val URI: String = "zcash:tmXuTnE11JojToagTqxXUn6KvdxDE3iLKbp?amount=1&message=Hello%20world!"
|
||||||
|
|
||||||
|
@ -23,7 +22,8 @@ class ZecRequestTest {
|
||||||
private val AMOUNT = Zatoshi(1)
|
private val AMOUNT = Zatoshi(1)
|
||||||
private val MESSAGE = ZecRequestMessage("Hello world!")
|
private val MESSAGE = ZecRequestMessage("Hello world!")
|
||||||
private const val ADDRESS_STRING = "tmXuTnE11JojToagTqxXUn6KvdxDE3iLKbp"
|
private const val ADDRESS_STRING = "tmXuTnE11JojToagTqxXUn6KvdxDE3iLKbp"
|
||||||
private val ADDRESS: WalletAddress.Unified = runBlocking {
|
private val ADDRESS: WalletAddress.Unified =
|
||||||
|
runBlocking {
|
||||||
WalletAddress.Unified.new(ADDRESS_STRING)
|
WalletAddress.Unified.new(ADDRESS_STRING)
|
||||||
}
|
}
|
||||||
val REQUEST = ZecRequest(ADDRESS, AMOUNT, MESSAGE)
|
val REQUEST = ZecRequest(ADDRESS, AMOUNT, MESSAGE)
|
||||||
|
@ -31,7 +31,8 @@ class ZecRequestTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@SmallTest
|
@SmallTest
|
||||||
fun parse_uri_not_null() = runTest {
|
fun parse_uri_not_null() =
|
||||||
|
runTest {
|
||||||
val parsed = ZecRequest.fromUri(Zip321UriParseFixture.URI)
|
val parsed = ZecRequest.fromUri(Zip321UriParseFixture.URI)
|
||||||
|
|
||||||
assertNotNull(parsed)
|
assertNotNull(parsed)
|
||||||
|
@ -39,7 +40,8 @@ class ZecRequestTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@SmallTest
|
@SmallTest
|
||||||
fun parse_uri_valid_result() = runTest {
|
fun parse_uri_valid_result() =
|
||||||
|
runTest {
|
||||||
val parsed = ZecRequest.fromUri(Zip321UriParseFixture.URI)
|
val parsed = ZecRequest.fromUri(Zip321UriParseFixture.URI)
|
||||||
|
|
||||||
assertTrue(parsed.message.value.length <= ZecRequestMessage.MAX_MESSAGE_LENGTH)
|
assertTrue(parsed.message.value.length <= ZecRequestMessage.MAX_MESSAGE_LENGTH)
|
||||||
|
@ -49,9 +51,11 @@ class ZecRequestTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@SmallTest
|
@SmallTest
|
||||||
fun parse_uri_correct_result() = runTest {
|
fun parse_uri_correct_result() =
|
||||||
|
runTest {
|
||||||
val parsed = ZecRequest.fromUri(Zip321UriParseFixture.URI)
|
val parsed = ZecRequest.fromUri(Zip321UriParseFixture.URI)
|
||||||
val expected = ZecRequest(
|
val expected =
|
||||||
|
ZecRequest(
|
||||||
WalletAddress.Unified.new(Zip321UriParseFixture.ADDRESS),
|
WalletAddress.Unified.new(Zip321UriParseFixture.ADDRESS),
|
||||||
Zip321UriParseFixture.AMOUNT,
|
Zip321UriParseFixture.AMOUNT,
|
||||||
Zip321UriParseFixture.MESSAGE
|
Zip321UriParseFixture.MESSAGE
|
||||||
|
@ -64,10 +68,12 @@ class ZecRequestTest {
|
||||||
@SmallTest
|
@SmallTest
|
||||||
// TODO [#397]: Waiting for an implementation of Uri parser in SDK project
|
// TODO [#397]: Waiting for an implementation of Uri parser in SDK project
|
||||||
@Ignore("Waiting for an implementation of Uri parser in SDK project")
|
@Ignore("Waiting for an implementation of Uri parser in SDK project")
|
||||||
fun parse_uri_incorrect_result() = runTest {
|
fun parse_uri_incorrect_result() =
|
||||||
|
runTest {
|
||||||
val parsed = ZecRequest.fromUri(URI)
|
val parsed = ZecRequest.fromUri(URI)
|
||||||
val expected = REQUEST
|
val expected = REQUEST
|
||||||
val actual = ZecRequest(
|
val actual =
|
||||||
|
ZecRequest(
|
||||||
WalletAddress.Unified.new(Zip321UriParseFixture.ADDRESS),
|
WalletAddress.Unified.new(Zip321UriParseFixture.ADDRESS),
|
||||||
Zip321UriParseFixture.AMOUNT,
|
Zip321UriParseFixture.AMOUNT,
|
||||||
Zip321UriParseFixture.MESSAGE
|
Zip321UriParseFixture.MESSAGE
|
||||||
|
@ -79,7 +85,8 @@ class ZecRequestTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@SmallTest
|
@SmallTest
|
||||||
fun build_uri_not_null() = runTest {
|
fun build_uri_not_null() =
|
||||||
|
runTest {
|
||||||
val request = Zip321UriBuildFixture.REQUEST
|
val request = Zip321UriBuildFixture.REQUEST
|
||||||
val built = request.toUri()
|
val built = request.toUri()
|
||||||
|
|
||||||
|
@ -88,7 +95,8 @@ class ZecRequestTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@SmallTest
|
@SmallTest
|
||||||
fun build_uri_valid_result() = runTest {
|
fun build_uri_valid_result() =
|
||||||
|
runTest {
|
||||||
val request = Zip321UriBuildFixture.REQUEST
|
val request = Zip321UriBuildFixture.REQUEST
|
||||||
val built = request.toUri()
|
val built = request.toUri()
|
||||||
|
|
||||||
|
@ -98,7 +106,8 @@ class ZecRequestTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@SmallTest
|
@SmallTest
|
||||||
fun built_uri_correct_result() = runTest {
|
fun built_uri_correct_result() =
|
||||||
|
runTest {
|
||||||
val request = Zip321UriBuildFixture.REQUEST
|
val request = Zip321UriBuildFixture.REQUEST
|
||||||
val built = request.toUri()
|
val built = request.toUri()
|
||||||
val expected = Zip321UriBuildFixture.URI
|
val expected = Zip321UriBuildFixture.URI
|
||||||
|
@ -110,7 +119,8 @@ class ZecRequestTest {
|
||||||
@SmallTest
|
@SmallTest
|
||||||
// TODO [#397]: Waiting for an implementation of Uri parser in SDK project
|
// TODO [#397]: Waiting for an implementation of Uri parser in SDK project
|
||||||
@Ignore("Waiting for an implementation of Uri parser in SDK project")
|
@Ignore("Waiting for an implementation of Uri parser in SDK project")
|
||||||
fun build_uri_incorrect_result() = runTest {
|
fun build_uri_incorrect_result() =
|
||||||
|
runTest {
|
||||||
val request = Zip321UriBuildFixture.REQUEST
|
val request = Zip321UriBuildFixture.REQUEST
|
||||||
val built = request.toUri()
|
val built = request.toUri()
|
||||||
val expected = URI
|
val expected = URI
|
||||||
|
|
|
@ -7,7 +7,6 @@ import org.junit.Test
|
||||||
import kotlin.test.assertNotSame
|
import kotlin.test.assertNotSame
|
||||||
|
|
||||||
class ZcashCurrencyTest {
|
class ZcashCurrencyTest {
|
||||||
|
|
||||||
@SmallTest
|
@SmallTest
|
||||||
@Test
|
@Test
|
||||||
fun check_is_zec_type() {
|
fun check_is_zec_type() {
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
@file:Suppress("ktlint:filename")
|
@file:Suppress("ktlint:standard:filename")
|
||||||
|
|
||||||
package cash.z.ecc.sdk.extension
|
package cash.z.ecc.sdk.extension
|
||||||
|
|
||||||
|
@ -12,9 +12,10 @@ fun PercentDecimal.toPercentageWithDecimal(decimalFormat: DecimalFormat = prepar
|
||||||
return decimalFormat.format(decimal * 100)
|
return decimalFormat.format(decimal * 100)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun preparePercentDecimalFormat(): DecimalFormat = DecimalFormat().apply {
|
private fun preparePercentDecimalFormat(): DecimalFormat =
|
||||||
|
DecimalFormat().apply {
|
||||||
val monetarySeparators = MonetarySeparators.current()
|
val monetarySeparators = MonetarySeparators.current()
|
||||||
val localizedPattern = "##0${monetarySeparators.decimal}00"
|
val localizedPattern = "##0${monetarySeparators.decimal}00"
|
||||||
applyLocalizedPattern(localizedPattern)
|
applyLocalizedPattern(localizedPattern)
|
||||||
roundingMode = RoundingMode.HALF_UP
|
roundingMode = RoundingMode.HALF_UP
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
@file:Suppress("ktlint:filename")
|
@file:Suppress("ktlint:standard:filename")
|
||||||
|
|
||||||
package cash.z.ecc.sdk.extension
|
package cash.z.ecc.sdk.extension
|
||||||
|
|
||||||
|
@ -6,7 +6,10 @@ import cash.z.ecc.android.sdk.Synchronizer
|
||||||
import cash.z.ecc.android.sdk.model.UnifiedSpendingKey
|
import cash.z.ecc.android.sdk.model.UnifiedSpendingKey
|
||||||
import cash.z.ecc.android.sdk.model.ZecSend
|
import cash.z.ecc.android.sdk.model.ZecSend
|
||||||
|
|
||||||
suspend fun Synchronizer.send(spendingKey: UnifiedSpendingKey, send: ZecSend) = sendToAddress(
|
suspend fun Synchronizer.send(
|
||||||
|
spendingKey: UnifiedSpendingKey,
|
||||||
|
send: ZecSend
|
||||||
|
) = sendToAddress(
|
||||||
spendingKey,
|
spendingKey,
|
||||||
send.amount,
|
send.amount,
|
||||||
send.destination.address,
|
send.destination.address,
|
||||||
|
|
|
@ -9,7 +9,6 @@ import cash.z.ecc.android.sdk.model.ZcashNetwork
|
||||||
import co.electriccoin.lightwallet.client.model.LightWalletEndpoint
|
import co.electriccoin.lightwallet.client.model.LightWalletEndpoint
|
||||||
|
|
||||||
object PersistableWalletFixture {
|
object PersistableWalletFixture {
|
||||||
|
|
||||||
val NETWORK = ZcashNetwork.Mainnet
|
val NETWORK = ZcashNetwork.Mainnet
|
||||||
|
|
||||||
val ENDPOINT = LightWalletEndpoint.Mainnet
|
val ENDPOINT = LightWalletEndpoint.Mainnet
|
||||||
|
|
|
@ -3,8 +3,9 @@ package cash.z.ecc.sdk.fixture
|
||||||
import cash.z.ecc.android.sdk.model.SeedPhrase
|
import cash.z.ecc.android.sdk.model.SeedPhrase
|
||||||
|
|
||||||
object SeedPhraseFixture {
|
object SeedPhraseFixture {
|
||||||
@Suppress("MaxLineLength")
|
const val SEED_PHRASE =
|
||||||
val SEED_PHRASE = "still champion voice habit trend flight survey between bitter process artefact blind carbon truly provide dizzy crush flush breeze blouse charge solid fish spread"
|
"still champion voice habit trend flight survey between bitter process artefact blind carbon " +
|
||||||
|
"truly provide dizzy crush flush breeze blouse charge solid fish spread"
|
||||||
|
|
||||||
fun new(seedPhrase: String = SEED_PHRASE) = SeedPhrase.new(seedPhrase)
|
fun new(seedPhrase: String = SEED_PHRASE) = SeedPhrase.new(seedPhrase)
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,13 +9,15 @@ import kotlinx.coroutines.runBlocking
|
||||||
|
|
||||||
object Zip321UriBuildFixture {
|
object Zip321UriBuildFixture {
|
||||||
// TODO [#161]: Pending SDK support
|
// TODO [#161]: Pending SDK support
|
||||||
const val URI: String = "zcash:Unified%20GitHub%20Issue%20#161?amount=123&message=Thank%20you%20" +
|
const val URI: String =
|
||||||
|
"zcash:Unified%20GitHub%20Issue%20#161?amount=123&message=Thank%20you%20" +
|
||||||
"for%20your%20purchase"
|
"for%20your%20purchase"
|
||||||
|
|
||||||
@Suppress("MagicNumber")
|
@Suppress("MagicNumber")
|
||||||
val AMOUNT = Zatoshi(123)
|
val AMOUNT = Zatoshi(123)
|
||||||
val MESSAGE = ZecRequestMessage("Thank you for your purchase")
|
val MESSAGE = ZecRequestMessage("Thank you for your purchase")
|
||||||
val ADDRESS: WalletAddress.Unified = runBlocking {
|
val ADDRESS: WalletAddress.Unified =
|
||||||
|
runBlocking {
|
||||||
WalletAddress.Unified.new(WalletAddressFixture.UNIFIED_ADDRESS_STRING)
|
WalletAddress.Unified.new(WalletAddressFixture.UNIFIED_ADDRESS_STRING)
|
||||||
}
|
}
|
||||||
val REQUEST = ZecRequest(ADDRESS, AMOUNT, MESSAGE)
|
val REQUEST = ZecRequest(ADDRESS, AMOUNT, MESSAGE)
|
||||||
|
|
|
@ -8,7 +8,8 @@ import cash.z.ecc.sdk.model.ZecRequestMessage
|
||||||
|
|
||||||
object Zip321UriParseFixture {
|
object Zip321UriParseFixture {
|
||||||
// TODO [#161]: Pending SDK support
|
// TODO [#161]: Pending SDK support
|
||||||
const val URI: String = "zcash:Unified%20GitHub%20Issue%20#161?amount=123&message=Thank%20you%20" +
|
const val URI: String =
|
||||||
|
"zcash:Unified%20GitHub%20Issue%20#161?amount=123&message=Thank%20you%20" +
|
||||||
"for%20your%20purchase"
|
"for%20your%20purchase"
|
||||||
|
|
||||||
const val ADDRESS: String = WalletAddressFixture.UNIFIED_ADDRESS_STRING
|
const val ADDRESS: String = WalletAddressFixture.UNIFIED_ADDRESS_STRING
|
||||||
|
@ -20,7 +21,5 @@ object Zip321UriParseFixture {
|
||||||
// TODO [#397]: Waiting for an implementation of Uri parser in SDK project
|
// TODO [#397]: Waiting for an implementation of Uri parser in SDK project
|
||||||
// Should return ZecRequest.fromUri(toParse) ideally, but it'd end up with an infinite loop for now.
|
// Should return ZecRequest.fromUri(toParse) ideally, but it'd end up with an infinite loop for now.
|
||||||
@Suppress("UNUSED_PARAMETER")
|
@Suppress("UNUSED_PARAMETER")
|
||||||
suspend fun new(
|
suspend fun new(toParse: String = URI) = ZecRequest(WalletAddress.Unified.new(ADDRESS), AMOUNT, MESSAGE)
|
||||||
toParse: String = URI
|
|
||||||
) = ZecRequest(WalletAddress.Unified.new(ADDRESS), AMOUNT, MESSAGE)
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,8 +10,11 @@ import java.util.Locale
|
||||||
// there as part of creating the object
|
// there as part of creating the object
|
||||||
sealed class SeedPhraseValidation {
|
sealed class SeedPhraseValidation {
|
||||||
object BadCount : SeedPhraseValidation()
|
object BadCount : SeedPhraseValidation()
|
||||||
|
|
||||||
object BadWord : SeedPhraseValidation()
|
object BadWord : SeedPhraseValidation()
|
||||||
|
|
||||||
object FailedChecksum : SeedPhraseValidation()
|
object FailedChecksum : SeedPhraseValidation()
|
||||||
|
|
||||||
class Valid(val seedPhrase: SeedPhrase) : SeedPhraseValidation()
|
class Valid(val seedPhrase: SeedPhrase) : SeedPhraseValidation()
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
|
@ -6,7 +6,6 @@ import cash.z.ecc.sdk.fixture.Zip321UriBuildFixture
|
||||||
import cash.z.ecc.sdk.fixture.Zip321UriParseFixture
|
import cash.z.ecc.sdk.fixture.Zip321UriParseFixture
|
||||||
|
|
||||||
data class ZecRequest(val address: WalletAddress.Unified, val amount: Zatoshi, val message: ZecRequestMessage) {
|
data class ZecRequest(val address: WalletAddress.Unified, val amount: Zatoshi, val message: ZecRequestMessage) {
|
||||||
|
|
||||||
// TODO [#397]: Waiting for an implementation of Uri parser in SDK project
|
// TODO [#397]: Waiting for an implementation of Uri parser in SDK project
|
||||||
// TODO [#397]: https://github.com/Electric-Coin-Company/zcash-android-wallet-sdk/issues/397
|
// TODO [#397]: https://github.com/Electric-Coin-Company/zcash-android-wallet-sdk/issues/397
|
||||||
suspend fun toUri(): String {
|
suspend fun toUri(): String {
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
@file:Suppress("ktlint:filename")
|
@file:Suppress("ktlint:standard:filename")
|
||||||
|
|
||||||
package cash.z.ecc.sdk.type
|
package cash.z.ecc.sdk.type
|
||||||
|
|
||||||
|
@ -21,6 +21,7 @@ import cash.z.ecc.sdk.ext.R
|
||||||
* - Using a ContentProvider for dynamic injection, where the URI is defined
|
* - Using a ContentProvider for dynamic injection, where the URI is defined
|
||||||
* - Using AndroidManifest metadata for dynamic injection
|
* - Using AndroidManifest metadata for dynamic injection
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return Zcash network determined from resources. A resource overlay of [R.bool.zcash_is_testnet]
|
* @return Zcash network determined from resources. A resource overlay of [R.bool.zcash_is_testnet]
|
||||||
* can be used for different build variants to change the network type.
|
* can be used for different build variants to change the network type.
|
||||||
|
|
|
@ -13,13 +13,16 @@ class AbstractProcessNameContentProviderTest {
|
||||||
@SmallTest
|
@SmallTest
|
||||||
fun getProcessName_from_provider_info() {
|
fun getProcessName_from_provider_info() {
|
||||||
val expectedApplicationProcessName = "beep" // $NON-NLS
|
val expectedApplicationProcessName = "beep" // $NON-NLS
|
||||||
val ctx: ContextWrapper = object : ContextWrapper(ApplicationProvider.getApplicationContext()) {
|
val ctx: ContextWrapper =
|
||||||
override fun getApplicationInfo() = ApplicationInfo().apply {
|
object : ContextWrapper(ApplicationProvider.getApplicationContext()) {
|
||||||
|
override fun getApplicationInfo() =
|
||||||
|
ApplicationInfo().apply {
|
||||||
processName = expectedApplicationProcessName
|
processName = expectedApplicationProcessName
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val actualProcessName = AbstractProcessNameContentProvider.getProcessNameLegacy(
|
val actualProcessName =
|
||||||
|
AbstractProcessNameContentProvider.getProcessNameLegacy(
|
||||||
ctx,
|
ctx,
|
||||||
ProviderInfo()
|
ProviderInfo()
|
||||||
)
|
)
|
||||||
|
|
|
@ -13,7 +13,8 @@ class VersionCodeCompatTest {
|
||||||
fun versionCodeCompat() {
|
fun versionCodeCompat() {
|
||||||
val expectedVersionCode = 123L
|
val expectedVersionCode = 123L
|
||||||
|
|
||||||
val packageInfo = PackageInfo().apply {
|
val packageInfo =
|
||||||
|
PackageInfo().apply {
|
||||||
@Suppress("Deprecation")
|
@Suppress("Deprecation")
|
||||||
versionCode = expectedVersionCode.toInt()
|
versionCode = expectedVersionCode.toInt()
|
||||||
if (AndroidApiVersion.isAtLeastT) {
|
if (AndroidApiVersion.isAtLeastT) {
|
||||||
|
|
|
@ -11,7 +11,9 @@ object AndroidApiVersion {
|
||||||
* [sdk].
|
* [sdk].
|
||||||
*/
|
*/
|
||||||
@ChecksSdkIntAtLeast(parameter = 0)
|
@ChecksSdkIntAtLeast(parameter = 0)
|
||||||
fun isAtLeast(@IntRange(from = Build.VERSION_CODES.BASE.toLong()) sdk: Int): Boolean {
|
fun isAtLeast(
|
||||||
|
@IntRange(from = Build.VERSION_CODES.BASE.toLong()) sdk: Int
|
||||||
|
): Boolean {
|
||||||
return Build.VERSION.SDK_INT >= sdk
|
return Build.VERSION.SDK_INT >= sdk
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
@file:Suppress("ktlint:filename")
|
@file:Suppress("ktlint:standard:filename")
|
||||||
|
|
||||||
package co.electriccoin.zcash.spackle
|
package co.electriccoin.zcash.spackle
|
||||||
|
|
||||||
|
@ -7,6 +7,7 @@ import android.content.ClipboardManager
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
suspend fun ClipboardManager.setPrimaryClipSuspend(data: ClipData) = withContext(Dispatchers.IO) {
|
suspend fun ClipboardManager.setPrimaryClipSuspend(data: ClipData) =
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
setPrimaryClip(data)
|
setPrimaryClip(data)
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,7 +7,6 @@ import android.widget.Toast
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
|
|
||||||
object ClipboardManagerUtil {
|
object ClipboardManagerUtil {
|
||||||
|
|
||||||
fun copyToClipboard(
|
fun copyToClipboard(
|
||||||
context: Context,
|
context: Context,
|
||||||
label: String,
|
label: String,
|
||||||
|
@ -15,7 +14,8 @@ object ClipboardManagerUtil {
|
||||||
) {
|
) {
|
||||||
Twig.info { "Copied to clipboard: label: $label, value: $value" }
|
Twig.info { "Copied to clipboard: label: $label, value: $value" }
|
||||||
val clipboardManager = context.getSystemService(ClipboardManager::class.java)
|
val clipboardManager = context.getSystemService(ClipboardManager::class.java)
|
||||||
val data = ClipData.newPlainText(
|
val data =
|
||||||
|
ClipData.newPlainText(
|
||||||
label,
|
label,
|
||||||
value
|
value
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
@file:Suppress("ktlint:filename")
|
@file:Suppress("ktlint:standard:filename")
|
||||||
|
|
||||||
package co.electriccoin.zcash.spackle
|
package co.electriccoin.zcash.spackle
|
||||||
|
|
||||||
|
@ -6,6 +6,7 @@ import android.content.Context
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
suspend fun Context.getExternalFilesDirSuspend(type: String?) = withContext(Dispatchers.IO) {
|
suspend fun Context.getExternalFilesDirSuspend(type: String?) =
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
getExternalFilesDir(type)
|
getExternalFilesDir(type)
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,7 +11,10 @@ import kotlinx.coroutines.launch
|
||||||
* It is not recommended to cancel this scope.
|
* It is not recommended to cancel this scope.
|
||||||
*/
|
*/
|
||||||
abstract class CoroutineBroadcastReceiver(private val broadcastReceiverScope: CoroutineScope) : BroadcastReceiver() {
|
abstract class CoroutineBroadcastReceiver(private val broadcastReceiverScope: CoroutineScope) : BroadcastReceiver() {
|
||||||
final override fun onReceive(context: Context, intent: Intent) {
|
final override fun onReceive(
|
||||||
|
context: Context,
|
||||||
|
intent: Intent
|
||||||
|
) {
|
||||||
val pendingResult = goAsync()
|
val pendingResult = goAsync()
|
||||||
|
|
||||||
broadcastReceiverScope.launch {
|
broadcastReceiverScope.launch {
|
||||||
|
@ -29,5 +32,8 @@ abstract class CoroutineBroadcastReceiver(private val broadcastReceiverScope: Co
|
||||||
* the Android timeout for broadcast receivers. This method is suitable for brief disk IO but
|
* the Android timeout for broadcast receivers. This method is suitable for brief disk IO but
|
||||||
* not suitable for network calls.
|
* not suitable for network calls.
|
||||||
*/
|
*/
|
||||||
abstract suspend fun onReceiveSuspend(context: Context, intent: Intent)
|
abstract suspend fun onReceiveSuspend(
|
||||||
|
context: Context,
|
||||||
|
intent: Intent
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,7 +11,8 @@ object EmulatorWtfUtil {
|
||||||
private const val EMULATOR_WTF_SETTING = "emulator.wtf" // $NON-NLS
|
private const val EMULATOR_WTF_SETTING = "emulator.wtf" // $NON-NLS
|
||||||
private const val SETTING_TRUE = "true" // $NON-NLS
|
private const val SETTING_TRUE = "true" // $NON-NLS
|
||||||
|
|
||||||
private val isEmulatorWtfCached = LazyWithArgument<Context, Boolean> {
|
private val isEmulatorWtfCached =
|
||||||
|
LazyWithArgument<Context, Boolean> {
|
||||||
isEmulatorWtfImpl(it)
|
isEmulatorWtfImpl(it)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -11,7 +11,8 @@ object FirebaseTestLabUtil {
|
||||||
private const val FIREBASE_TEST_LAB_SETTING = "firebase.test.lab" // $NON-NLS
|
private const val FIREBASE_TEST_LAB_SETTING = "firebase.test.lab" // $NON-NLS
|
||||||
private const val SETTING_TRUE = "true" // $NON-NLS
|
private const val SETTING_TRUE = "true" // $NON-NLS
|
||||||
|
|
||||||
private val isFirebaseTestLabCached = LazyWithArgument<Context, Boolean> {
|
private val isFirebaseTestLabCached =
|
||||||
|
LazyWithArgument<Context, Boolean> {
|
||||||
isFirebaseTestLabImpl(it)
|
isFirebaseTestLabImpl(it)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -24,10 +25,10 @@ object FirebaseTestLabUtil {
|
||||||
/*
|
/*
|
||||||
* Per the documentation at https://firebase.google.com/docs/test-lab/android-studio
|
* Per the documentation at https://firebase.google.com/docs/test-lab/android-studio
|
||||||
*/
|
*/
|
||||||
|
return runCatching {
|
||||||
// Tested with the benchmark library, this is very fast. There shouldn't be a need to make
|
// Tested with the benchmark library, this is very fast. There shouldn't be a need to make
|
||||||
// this a suspend function. That said, we'll still cache the result as a just-in-case
|
// this a suspend function. That said, we'll still cache the result as a just-in-case
|
||||||
// since IPC may be involved.
|
// since IPC may be involved.
|
||||||
return runCatching {
|
|
||||||
SETTING_TRUE == Settings.System.getString(context.contentResolver, FIREBASE_TEST_LAB_SETTING)
|
SETTING_TRUE == Settings.System.getString(context.contentResolver, FIREBASE_TEST_LAB_SETTING)
|
||||||
}.recover {
|
}.recover {
|
||||||
// Fail-safe in case an error occurs
|
// Fail-safe in case an error occurs
|
||||||
|
|
|
@ -7,14 +7,20 @@ import android.os.Build
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
fun PackageManager.getPackageInfoCompat(packageName: String, flags: Long): PackageInfo =
|
fun PackageManager.getPackageInfoCompat(
|
||||||
|
packageName: String,
|
||||||
|
flags: Long
|
||||||
|
): PackageInfo =
|
||||||
if (AndroidApiVersion.isAtLeastT) {
|
if (AndroidApiVersion.isAtLeastT) {
|
||||||
getPackageInfoTPlus(packageName, flags)
|
getPackageInfoTPlus(packageName, flags)
|
||||||
} else {
|
} else {
|
||||||
getPackageInfoLegacy(packageName, flags)
|
getPackageInfoLegacy(packageName, flags)
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun PackageManager.getPackageInfoCompatSuspend(packageName: String, flags: Long): PackageInfo =
|
suspend fun PackageManager.getPackageInfoCompatSuspend(
|
||||||
|
packageName: String,
|
||||||
|
flags: Long
|
||||||
|
): PackageInfo =
|
||||||
if (AndroidApiVersion.isAtLeastT) {
|
if (AndroidApiVersion.isAtLeastT) {
|
||||||
withContext(Dispatchers.IO) { getPackageInfoTPlus(packageName, flags) }
|
withContext(Dispatchers.IO) { getPackageInfoTPlus(packageName, flags) }
|
||||||
} else {
|
} else {
|
||||||
|
@ -22,9 +28,13 @@ suspend fun PackageManager.getPackageInfoCompatSuspend(packageName: String, flag
|
||||||
}
|
}
|
||||||
|
|
||||||
@TargetApi(Build.VERSION_CODES.TIRAMISU)
|
@TargetApi(Build.VERSION_CODES.TIRAMISU)
|
||||||
private fun PackageManager.getPackageInfoTPlus(packageName: String, flags: Long) =
|
private fun PackageManager.getPackageInfoTPlus(
|
||||||
getPackageInfo(packageName, PackageManager.PackageInfoFlags.of(flags))
|
packageName: String,
|
||||||
|
flags: Long
|
||||||
|
) = getPackageInfo(packageName, PackageManager.PackageInfoFlags.of(flags))
|
||||||
|
|
||||||
@Suppress("Deprecation")
|
@Suppress("Deprecation")
|
||||||
private fun PackageManager.getPackageInfoLegacy(packageName: String, flags: Long) =
|
private fun PackageManager.getPackageInfoLegacy(
|
||||||
getPackageInfo(packageName, flags.toInt())
|
packageName: String,
|
||||||
|
flags: Long
|
||||||
|
) = getPackageInfo(packageName, flags.toInt())
|
||||||
|
|
|
@ -4,7 +4,6 @@ import android.annotation.SuppressLint
|
||||||
import android.os.StrictMode
|
import android.os.StrictMode
|
||||||
|
|
||||||
object StrictModeCompat {
|
object StrictModeCompat {
|
||||||
|
|
||||||
fun enableStrictMode(isCrashOnViolation: Boolean) {
|
fun enableStrictMode(isCrashOnViolation: Boolean) {
|
||||||
configureStrictMode(isCrashOnViolation)
|
configureStrictMode(isCrashOnViolation)
|
||||||
}
|
}
|
||||||
|
|
|
@ -40,7 +40,10 @@ object Twig {
|
||||||
|
|
||||||
// JVMStatic is to simplify ProGuard/R8 rules for stripping this
|
// JVMStatic is to simplify ProGuard/R8 rules for stripping this
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
fun verbose(throwable: Throwable, message: () -> String) {
|
fun verbose(
|
||||||
|
throwable: Throwable,
|
||||||
|
message: () -> String
|
||||||
|
) {
|
||||||
Log.v(tag, formatMessage(message), throwable)
|
Log.v(tag, formatMessage(message), throwable)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -52,7 +55,10 @@ object Twig {
|
||||||
|
|
||||||
// JVMStatic is to simplify ProGuard/R8 rules for stripping this
|
// JVMStatic is to simplify ProGuard/R8 rules for stripping this
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
fun debug(throwable: Throwable, message: () -> String) {
|
fun debug(
|
||||||
|
throwable: Throwable,
|
||||||
|
message: () -> String
|
||||||
|
) {
|
||||||
Log.d(tag, formatMessage(message), throwable)
|
Log.d(tag, formatMessage(message), throwable)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -64,7 +70,10 @@ object Twig {
|
||||||
|
|
||||||
// JVMStatic is to simplify ProGuard/R8 rules for stripping this
|
// JVMStatic is to simplify ProGuard/R8 rules for stripping this
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
fun info(throwable: Throwable, message: () -> String) {
|
fun info(
|
||||||
|
throwable: Throwable,
|
||||||
|
message: () -> String
|
||||||
|
) {
|
||||||
Log.i(tag, formatMessage(message), throwable)
|
Log.i(tag, formatMessage(message), throwable)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -76,7 +85,10 @@ object Twig {
|
||||||
|
|
||||||
// JVMStatic is to simplify ProGuard/R8 rules for stripping this
|
// JVMStatic is to simplify ProGuard/R8 rules for stripping this
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
fun warn(throwable: Throwable, message: () -> String) {
|
fun warn(
|
||||||
|
throwable: Throwable,
|
||||||
|
message: () -> String
|
||||||
|
) {
|
||||||
Log.w(tag, formatMessage(message), throwable)
|
Log.w(tag, formatMessage(message), throwable)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -88,7 +100,10 @@ object Twig {
|
||||||
|
|
||||||
// JVMStatic is to simplify ProGuard/R8 rules for stripping this
|
// JVMStatic is to simplify ProGuard/R8 rules for stripping this
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
fun error(throwable: Throwable, message: () -> String) {
|
fun error(
|
||||||
|
throwable: Throwable,
|
||||||
|
message: () -> String
|
||||||
|
) {
|
||||||
Log.e(tag, formatMessage(message), throwable)
|
Log.e(tag, formatMessage(message), throwable)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -96,11 +111,12 @@ object Twig {
|
||||||
* Can be called in a release build to test that `assumenosideeffects` ProGuard rules have been
|
* Can be called in a release build to test that `assumenosideeffects` ProGuard rules have been
|
||||||
* properly processed to strip out logging messages.
|
* properly processed to strip out logging messages.
|
||||||
*/
|
*/
|
||||||
// JVMStatic is to simplify ProGuard/R8 rules for stripping this
|
@JvmStatic // JVMStatic is to simplify ProGuard/R8 rules for stripping this
|
||||||
@JvmStatic
|
|
||||||
fun assertLoggingStripped() {
|
fun assertLoggingStripped() {
|
||||||
@Suppress("MaxLineLength")
|
throw AssertionError(
|
||||||
throw AssertionError("Logging was not disabled by ProGuard or R8. Logging should be disabled in release builds to reduce risk of sensitive information being leaked.") // $NON-NLS-1$
|
"Logging was not disabled by ProGuard or R8. Logging should be disabled in release builds to reduce risk " +
|
||||||
|
"of sensitive information being leaked."
|
||||||
|
) // $NON-NLS-1$
|
||||||
}
|
}
|
||||||
|
|
||||||
private const val CALL_DEPTH = 4
|
private const val CALL_DEPTH = 4
|
||||||
|
|
|
@ -5,7 +5,8 @@ import android.content.pm.PackageInfo
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
|
|
||||||
val PackageInfo.versionCodeCompat
|
val PackageInfo.versionCodeCompat
|
||||||
get() = if (AndroidApiVersion.isAtLeastP) {
|
get() =
|
||||||
|
if (AndroidApiVersion.isAtLeastP) {
|
||||||
getVersionCodePPlus()
|
getVersionCodePPlus()
|
||||||
} else {
|
} else {
|
||||||
versionCodeLegacy.toLong()
|
versionCodeLegacy.toLong()
|
||||||
|
|
|
@ -20,10 +20,14 @@ import co.electriccoin.zcash.spackle.AndroidApiVersion
|
||||||
open class AbstractProcessNameContentProvider : ContentProvider() {
|
open class AbstractProcessNameContentProvider : ContentProvider() {
|
||||||
override fun onCreate() = true
|
override fun onCreate() = true
|
||||||
|
|
||||||
override fun attachInfo(context: Context, info: ProviderInfo) {
|
override fun attachInfo(
|
||||||
|
context: Context,
|
||||||
|
info: ProviderInfo
|
||||||
|
) {
|
||||||
super.attachInfo(context, info)
|
super.attachInfo(context, info)
|
||||||
|
|
||||||
val processName: String = if (AndroidApiVersion.isAtLeastT) {
|
val processName: String =
|
||||||
|
if (AndroidApiVersion.isAtLeastT) {
|
||||||
getProcessNameTPlus()
|
getProcessNameTPlus()
|
||||||
} else if (AndroidApiVersion.isAtLeastP) {
|
} else if (AndroidApiVersion.isAtLeastP) {
|
||||||
getProcessNamePPlus()
|
getProcessNamePPlus()
|
||||||
|
@ -54,11 +58,18 @@ open class AbstractProcessNameContentProvider : ContentProvider() {
|
||||||
throw UnsupportedOperationException()
|
throw UnsupportedOperationException()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun insert(uri: Uri, values: ContentValues?): Uri? {
|
override fun insert(
|
||||||
|
uri: Uri,
|
||||||
|
values: ContentValues?
|
||||||
|
): Uri? {
|
||||||
throw UnsupportedOperationException()
|
throw UnsupportedOperationException()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun delete(uri: Uri, selection: String?, selectionArgs: Array<out String>?): Int {
|
override fun delete(
|
||||||
|
uri: Uri,
|
||||||
|
selection: String?,
|
||||||
|
selectionArgs: Array<out String>?
|
||||||
|
): Int {
|
||||||
throw UnsupportedOperationException()
|
throw UnsupportedOperationException()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -72,7 +83,9 @@ open class AbstractProcessNameContentProvider : ContentProvider() {
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
internal fun getProcessNameLegacy(context: Context, info: ProviderInfo) =
|
internal fun getProcessNameLegacy(
|
||||||
info.processName ?: context.applicationInfo.processName ?: context.packageName
|
context: Context,
|
||||||
|
info: ProviderInfo
|
||||||
|
) = info.processName ?: context.applicationInfo.processName ?: context.packageName
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,7 +20,6 @@ import co.electriccoin.zcash.spackle.process.ProcessNameCompat.getProcessName
|
||||||
* way to get process name on older Android versions.
|
* way to get process name on older Android versions.
|
||||||
*/
|
*/
|
||||||
object ProcessNameCompat {
|
object ProcessNameCompat {
|
||||||
|
|
||||||
// GuardedBy intrinsicLock
|
// GuardedBy intrinsicLock
|
||||||
private var processName: String? = null
|
private var processName: String? = null
|
||||||
|
|
||||||
|
|
|
@ -4,7 +4,6 @@ import kotlin.test.Test
|
||||||
import kotlin.test.assertFailsWith
|
import kotlin.test.assertFailsWith
|
||||||
|
|
||||||
class ProgressTest {
|
class ProgressTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun last_greater_than_zero() {
|
fun last_greater_than_zero() {
|
||||||
assertFailsWith(IllegalArgumentException::class) {
|
assertFailsWith(IllegalArgumentException::class) {
|
||||||
|
|
|
@ -8,37 +8,45 @@ import java.io.File
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
|
|
||||||
suspend fun File.existsSuspend() = withContext(Dispatchers.IO) {
|
suspend fun File.existsSuspend() =
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
exists()
|
exists()
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun File.mkdirsSuspend() = withContext(Dispatchers.IO) {
|
suspend fun File.mkdirsSuspend() =
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
mkdirs()
|
mkdirs()
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun File.isDirectorySuspend() = withContext(Dispatchers.IO) {
|
suspend fun File.isDirectorySuspend() =
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
isDirectory
|
isDirectory
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun File.isFileSuspend() = withContext(Dispatchers.IO) {
|
suspend fun File.isFileSuspend() =
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
isFile
|
isFile
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun File.canWriteSuspend() = withContext(Dispatchers.IO) {
|
suspend fun File.canWriteSuspend() =
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
canWrite()
|
canWrite()
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun File.deleteSuspend() = withContext(Dispatchers.IO) {
|
suspend fun File.deleteSuspend() =
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
delete()
|
delete()
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun File.renameToSuspend(destination: File) = withContext(Dispatchers.IO) {
|
suspend fun File.renameToSuspend(destination: File) =
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
renameTo(destination)
|
renameTo(destination)
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun File.listFilesSuspend() = withContext(Dispatchers.IO) {
|
suspend fun File.listFilesSuspend() =
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
listFiles()
|
listFiles()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Given an ultimate output file destination, this generates a temporary file that [action] can write to. After action
|
* Given an ultimate output file destination, this generates a temporary file that [action] can write to. After action
|
||||||
|
@ -50,7 +58,8 @@ suspend fun File.listFilesSuspend() = withContext(Dispatchers.IO) {
|
||||||
* delete, rename, or do other operations in the filesystem.
|
* delete, rename, or do other operations in the filesystem.
|
||||||
*/
|
*/
|
||||||
suspend fun File.writeAtomically(action: (suspend (File) -> Unit)) {
|
suspend fun File.writeAtomically(action: (suspend (File) -> Unit)) {
|
||||||
val tempFile = withContext(Dispatchers.IO) {
|
val tempFile =
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
File(parentFile, name.newTempFileName()).also {
|
File(parentFile, name.newTempFileName()).also {
|
||||||
it.deleteOnExit()
|
it.deleteOnExit()
|
||||||
}
|
}
|
||||||
|
@ -61,7 +70,9 @@ suspend fun File.writeAtomically(action: (suspend (File) -> Unit)) {
|
||||||
try {
|
try {
|
||||||
action(tempFile)
|
action(tempFile)
|
||||||
isWriteSuccessful = true
|
isWriteSuccessful = true
|
||||||
} catch (@Suppress("TooGenericExceptionCaught") e: Exception) {
|
} catch (
|
||||||
|
@Suppress("TooGenericExceptionCaught") e: Exception
|
||||||
|
) {
|
||||||
tempFile.deleteSuspend()
|
tempFile.deleteSuspend()
|
||||||
throw e
|
throw e
|
||||||
} finally {
|
} finally {
|
||||||
|
|
|
@ -16,7 +16,8 @@ class WriteAtomicallyTest {
|
||||||
private fun newFile() = File(File("build"), "atomic_file_test-${UUID.randomUUID()}")
|
private fun newFile() = File(File("build"), "atomic_file_test-${UUID.randomUUID()}")
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `file has temp name`() = runTest {
|
fun `file has temp name`() =
|
||||||
|
runTest {
|
||||||
val testFile = newFile()
|
val testFile = newFile()
|
||||||
try {
|
try {
|
||||||
testFile.writeAtomically {
|
testFile.writeAtomically {
|
||||||
|
@ -29,7 +30,8 @@ class WriteAtomicallyTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `temp file deleted`() = runTest {
|
fun `temp file deleted`() =
|
||||||
|
runTest {
|
||||||
val testFile = newFile()
|
val testFile = newFile()
|
||||||
try {
|
try {
|
||||||
var tempFile: File? = null
|
var tempFile: File? = null
|
||||||
|
@ -47,7 +49,8 @@ class WriteAtomicallyTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `file is renamed`() = runTest {
|
fun `file is renamed`() =
|
||||||
|
runTest {
|
||||||
val testFile = newFile()
|
val testFile = newFile()
|
||||||
try {
|
try {
|
||||||
testFile.writeAtomically {
|
testFile.writeAtomically {
|
||||||
|
|
|
@ -10,8 +10,9 @@ import org.junit.Before
|
||||||
* Subclass this in view unit and integration tests. This verifies that
|
* Subclass this in view unit and integration tests. This verifies that
|
||||||
* prerequisites necessary for reliable UI tests are met, and it provides more useful error messages.
|
* prerequisites necessary for reliable UI tests are met, and it provides more useful error messages.
|
||||||
*/
|
*/
|
||||||
// Originally hoped to put this into ZcashUiTestRunner, although it causes reporting of test results to fail
|
|
||||||
open class UiTestPrerequisites {
|
open class UiTestPrerequisites {
|
||||||
|
// Originally hoped to put this into ZcashUiTestRunner, although it causes reporting of test results to fail
|
||||||
|
|
||||||
@Before
|
@Before
|
||||||
fun verifyPrerequisites() {
|
fun verifyPrerequisites() {
|
||||||
assertScreenIsOn()
|
assertScreenIsOn()
|
||||||
|
@ -26,7 +27,8 @@ open class UiTestPrerequisites {
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun isScreenOn(): Boolean {
|
private fun isScreenOn(): Boolean {
|
||||||
val powerService = ApplicationProvider.getApplicationContext<Context>()
|
val powerService =
|
||||||
|
ApplicationProvider.getApplicationContext<Context>()
|
||||||
.getSystemService(Context.POWER_SERVICE) as PowerManager
|
.getSystemService(Context.POWER_SERVICE) as PowerManager
|
||||||
return powerService.isInteractive
|
return powerService.isInteractive
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,7 +12,8 @@ class ZcashUiTestRunner : AndroidJUnitRunner() {
|
||||||
override fun onCreate(arguments: Bundle?) {
|
override fun onCreate(arguments: Bundle?) {
|
||||||
super.onCreate(arguments)
|
super.onCreate(arguments)
|
||||||
|
|
||||||
val powerManager = ApplicationProvider.getApplicationContext<Context>()
|
val powerManager =
|
||||||
|
ApplicationProvider.getApplicationContext<Context>()
|
||||||
.getSystemService(Context.POWER_SERVICE) as PowerManager
|
.getSystemService(Context.POWER_SERVICE) as PowerManager
|
||||||
|
|
||||||
// There is no alternative to this deprecated API. The suggestion of a view to keep the screen
|
// There is no alternative to this deprecated API. The suggestion of a view to keep the screen
|
||||||
|
|
|
@ -3,3 +3,5 @@ root = true
|
||||||
[*.{kt,kts}]
|
[*.{kt,kts}]
|
||||||
ktlint_standard_trailing-comma-on-call-site = disabled
|
ktlint_standard_trailing-comma-on-call-site = disabled
|
||||||
ktlint_standard_trailing-comma-on-declaration-site = disabled
|
ktlint_standard_trailing-comma-on-declaration-site = disabled
|
||||||
|
# When using Compose, suppress the `function-naming` rule in favor of PascalCase naming convention
|
||||||
|
ktlint_function_naming_ignore_when_annotated_with=Composable
|
|
@ -18,7 +18,6 @@ import org.junit.Test
|
||||||
* version and later on.
|
* version and later on.
|
||||||
*/
|
*/
|
||||||
class BasicStartupBenchmark {
|
class BasicStartupBenchmark {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val APP_TARGET_PACKAGE_NAME = "co.electriccoin.zcash"
|
private const val APP_TARGET_PACKAGE_NAME = "co.electriccoin.zcash"
|
||||||
}
|
}
|
||||||
|
@ -27,7 +26,8 @@ class BasicStartupBenchmark {
|
||||||
val benchmarkRule = MacrobenchmarkRule()
|
val benchmarkRule = MacrobenchmarkRule()
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun startup() = benchmarkRule.measureRepeated(
|
fun startup() =
|
||||||
|
benchmarkRule.measureRepeated(
|
||||||
packageName = APP_TARGET_PACKAGE_NAME,
|
packageName = APP_TARGET_PACKAGE_NAME,
|
||||||
metrics = listOf(StartupTimingMetric()),
|
metrics = listOf(StartupTimingMetric()),
|
||||||
iterations = 5,
|
iterations = 5,
|
||||||
|
|
|
@ -59,7 +59,8 @@ fun PrimaryButton(
|
||||||
onClick: () -> Unit,
|
onClick: () -> Unit,
|
||||||
text: String,
|
text: String,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
outerPaddingValues: PaddingValues = PaddingValues(
|
outerPaddingValues: PaddingValues =
|
||||||
|
PaddingValues(
|
||||||
horizontal = ZcashTheme.dimens.spacingNone,
|
horizontal = ZcashTheme.dimens.spacingNone,
|
||||||
vertical = ZcashTheme.dimens.spacingSmall
|
vertical = ZcashTheme.dimens.spacingSmall
|
||||||
),
|
),
|
||||||
|
@ -70,7 +71,8 @@ fun PrimaryButton(
|
||||||
Button(
|
Button(
|
||||||
shape = RectangleShape,
|
shape = RectangleShape,
|
||||||
enabled = enabled,
|
enabled = enabled,
|
||||||
modifier = modifier.then(Modifier.fillMaxWidth())
|
modifier =
|
||||||
|
modifier.then(Modifier.fillMaxWidth())
|
||||||
.padding(outerPaddingValues)
|
.padding(outerPaddingValues)
|
||||||
.shadow(
|
.shadow(
|
||||||
contentColor = textColor,
|
contentColor = textColor,
|
||||||
|
@ -81,12 +83,14 @@ fun PrimaryButton(
|
||||||
spread = ZcashTheme.dimens.buttonShadowSpread,
|
spread = ZcashTheme.dimens.buttonShadowSpread,
|
||||||
)
|
)
|
||||||
.translationClick(
|
.translationClick(
|
||||||
translationX = ZcashTheme.dimens.buttonShadowOffsetX + 6.dp, // + 6dp to exactly cover the bottom shadow
|
// + 6dp to exactly cover the bottom shadow
|
||||||
|
translationX = ZcashTheme.dimens.buttonShadowOffsetX + 6.dp,
|
||||||
translationY = ZcashTheme.dimens.buttonShadowOffsetX + 6.dp
|
translationY = ZcashTheme.dimens.buttonShadowOffsetX + 6.dp
|
||||||
)
|
)
|
||||||
.defaultMinSize(ZcashTheme.dimens.buttonWidth, ZcashTheme.dimens.buttonHeight)
|
.defaultMinSize(ZcashTheme.dimens.buttonWidth, ZcashTheme.dimens.buttonHeight)
|
||||||
.border(1.dp, Color.Black),
|
.border(1.dp, Color.Black),
|
||||||
colors = buttonColors(
|
colors =
|
||||||
|
buttonColors(
|
||||||
containerColor = buttonColor,
|
containerColor = buttonColor,
|
||||||
disabledContainerColor = ZcashTheme.colors.disabledButtonColor,
|
disabledContainerColor = ZcashTheme.colors.disabledButtonColor,
|
||||||
disabledContentColor = ZcashTheme.colors.disabledButtonTextColor
|
disabledContentColor = ZcashTheme.colors.disabledButtonTextColor
|
||||||
|
@ -108,7 +112,8 @@ fun SecondaryButton(
|
||||||
onClick: () -> Unit,
|
onClick: () -> Unit,
|
||||||
text: String,
|
text: String,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
outerPaddingValues: PaddingValues = PaddingValues(
|
outerPaddingValues: PaddingValues =
|
||||||
|
PaddingValues(
|
||||||
horizontal = ZcashTheme.dimens.spacingNone,
|
horizontal = ZcashTheme.dimens.spacingNone,
|
||||||
vertical = ZcashTheme.dimens.spacingSmall
|
vertical = ZcashTheme.dimens.spacingSmall
|
||||||
),
|
),
|
||||||
|
@ -119,7 +124,8 @@ fun SecondaryButton(
|
||||||
Button(
|
Button(
|
||||||
shape = RectangleShape,
|
shape = RectangleShape,
|
||||||
enabled = enabled,
|
enabled = enabled,
|
||||||
modifier = modifier.then(Modifier.fillMaxWidth())
|
modifier =
|
||||||
|
modifier.then(Modifier.fillMaxWidth())
|
||||||
.padding(outerPaddingValues)
|
.padding(outerPaddingValues)
|
||||||
.shadow(
|
.shadow(
|
||||||
contentColor = textColor,
|
contentColor = textColor,
|
||||||
|
@ -129,12 +135,14 @@ fun SecondaryButton(
|
||||||
spread = ZcashTheme.dimens.buttonShadowSpread,
|
spread = ZcashTheme.dimens.buttonShadowSpread,
|
||||||
)
|
)
|
||||||
.translationClick(
|
.translationClick(
|
||||||
translationX = ZcashTheme.dimens.buttonShadowOffsetX + 6.dp, // + 6dp to exactly cover the bottom shadow
|
// + 6dp to exactly cover the bottom shadow
|
||||||
|
translationX = ZcashTheme.dimens.buttonShadowOffsetX + 6.dp,
|
||||||
translationY = ZcashTheme.dimens.buttonShadowOffsetX + 6.dp
|
translationY = ZcashTheme.dimens.buttonShadowOffsetX + 6.dp
|
||||||
)
|
)
|
||||||
.defaultMinSize(ZcashTheme.dimens.buttonWidth, ZcashTheme.dimens.buttonHeight)
|
.defaultMinSize(ZcashTheme.dimens.buttonWidth, ZcashTheme.dimens.buttonHeight)
|
||||||
.border(1.dp, Color.Black),
|
.border(1.dp, Color.Black),
|
||||||
colors = buttonColors(
|
colors =
|
||||||
|
buttonColors(
|
||||||
containerColor = buttonColor,
|
containerColor = buttonColor,
|
||||||
disabledContainerColor = ZcashTheme.colors.disabledButtonColor,
|
disabledContainerColor = ZcashTheme.colors.disabledButtonColor,
|
||||||
disabledContentColor = ZcashTheme.colors.disabledButtonTextColor
|
disabledContentColor = ZcashTheme.colors.disabledButtonTextColor
|
||||||
|
@ -155,7 +163,8 @@ fun NavigationButton(
|
||||||
onClick: () -> Unit,
|
onClick: () -> Unit,
|
||||||
text: String,
|
text: String,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
outerPaddingValues: PaddingValues = PaddingValues(
|
outerPaddingValues: PaddingValues =
|
||||||
|
PaddingValues(
|
||||||
horizontal = ZcashTheme.dimens.spacingDefault,
|
horizontal = ZcashTheme.dimens.spacingDefault,
|
||||||
vertical = ZcashTheme.dimens.spacingSmall
|
vertical = ZcashTheme.dimens.spacingSmall
|
||||||
),
|
),
|
||||||
|
@ -163,7 +172,8 @@ fun NavigationButton(
|
||||||
Button(
|
Button(
|
||||||
shape = RectangleShape,
|
shape = RectangleShape,
|
||||||
onClick = onClick,
|
onClick = onClick,
|
||||||
modifier = modifier.then(
|
modifier =
|
||||||
|
modifier.then(
|
||||||
Modifier
|
Modifier
|
||||||
.padding(outerPaddingValues)
|
.padding(outerPaddingValues)
|
||||||
),
|
),
|
||||||
|
@ -183,7 +193,8 @@ fun TertiaryButton(
|
||||||
onClick: () -> Unit,
|
onClick: () -> Unit,
|
||||||
text: String,
|
text: String,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
outerPaddingValues: PaddingValues = PaddingValues(
|
outerPaddingValues: PaddingValues =
|
||||||
|
PaddingValues(
|
||||||
horizontal = ZcashTheme.dimens.spacingDefault,
|
horizontal = ZcashTheme.dimens.spacingDefault,
|
||||||
vertical = ZcashTheme.dimens.spacingSmall
|
vertical = ZcashTheme.dimens.spacingSmall
|
||||||
),
|
),
|
||||||
|
@ -192,7 +203,8 @@ fun TertiaryButton(
|
||||||
Button(
|
Button(
|
||||||
shape = RectangleShape,
|
shape = RectangleShape,
|
||||||
onClick = onClick,
|
onClick = onClick,
|
||||||
modifier = modifier.then(
|
modifier =
|
||||||
|
modifier.then(
|
||||||
Modifier
|
Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(outerPaddingValues)
|
.padding(outerPaddingValues)
|
||||||
|
@ -215,7 +227,8 @@ fun DangerousButton(
|
||||||
onClick: () -> Unit,
|
onClick: () -> Unit,
|
||||||
text: String,
|
text: String,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
outerPaddingValues: PaddingValues = PaddingValues(
|
outerPaddingValues: PaddingValues =
|
||||||
|
PaddingValues(
|
||||||
horizontal = ZcashTheme.dimens.spacingDefault,
|
horizontal = ZcashTheme.dimens.spacingDefault,
|
||||||
vertical = ZcashTheme.dimens.spacingSmall
|
vertical = ZcashTheme.dimens.spacingSmall
|
||||||
),
|
),
|
||||||
|
@ -223,7 +236,8 @@ fun DangerousButton(
|
||||||
Button(
|
Button(
|
||||||
shape = RectangleShape,
|
shape = RectangleShape,
|
||||||
onClick = onClick,
|
onClick = onClick,
|
||||||
modifier = modifier.then(
|
modifier =
|
||||||
|
modifier.then(
|
||||||
Modifier
|
Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(outerPaddingValues)
|
.padding(outerPaddingValues)
|
||||||
|
@ -291,6 +305,7 @@ fun Modifier.shadow(
|
||||||
)
|
)
|
||||||
|
|
||||||
private enum class ButtonState { Pressed, Idle }
|
private enum class ButtonState { Pressed, Idle }
|
||||||
|
|
||||||
fun Modifier.translationClick(
|
fun Modifier.translationClick(
|
||||||
translationX: Dp = 0.dp,
|
translationX: Dp = 0.dp,
|
||||||
translationY: Dp = 0.dp
|
translationY: Dp = 0.dp
|
||||||
|
@ -298,24 +313,28 @@ fun Modifier.translationClick(
|
||||||
var buttonState by remember { mutableStateOf(ButtonState.Idle) }
|
var buttonState by remember { mutableStateOf(ButtonState.Idle) }
|
||||||
|
|
||||||
val translationXAnimated by animateFloatAsState(
|
val translationXAnimated by animateFloatAsState(
|
||||||
targetValue = if (buttonState == ButtonState.Pressed) {
|
targetValue =
|
||||||
|
if (buttonState == ButtonState.Pressed) {
|
||||||
translationX.value
|
translationX.value
|
||||||
} else {
|
} else {
|
||||||
0f
|
0f
|
||||||
},
|
},
|
||||||
label = "ClickTranslationXAnimation",
|
label = "ClickTranslationXAnimation",
|
||||||
animationSpec = tween(
|
animationSpec =
|
||||||
|
tween(
|
||||||
durationMillis = 100
|
durationMillis = 100
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
val translationYAnimated by animateFloatAsState(
|
val translationYAnimated by animateFloatAsState(
|
||||||
targetValue = if (buttonState == ButtonState.Pressed) {
|
targetValue =
|
||||||
|
if (buttonState == ButtonState.Pressed) {
|
||||||
translationY.value
|
translationY.value
|
||||||
} else {
|
} else {
|
||||||
0f
|
0f
|
||||||
},
|
},
|
||||||
label = "ClickTranslationYAnimation",
|
label = "ClickTranslationYAnimation",
|
||||||
animationSpec = tween(
|
animationSpec =
|
||||||
|
tween(
|
||||||
durationMillis = 100
|
durationMillis = 100
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
@ -327,7 +346,8 @@ fun Modifier.translationClick(
|
||||||
}
|
}
|
||||||
.pointerInput(buttonState) {
|
.pointerInput(buttonState) {
|
||||||
awaitPointerEventScope {
|
awaitPointerEventScope {
|
||||||
buttonState = if (buttonState == ButtonState.Pressed) {
|
buttonState =
|
||||||
|
if (buttonState == ButtonState.Pressed) {
|
||||||
waitForUpOrCancellation()
|
waitForUpOrCancellation()
|
||||||
ButtonState.Idle
|
ButtonState.Idle
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -41,7 +41,8 @@ fun CheckBox(
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
) {
|
) {
|
||||||
val checkBoxModifier = Modifier
|
val checkBoxModifier =
|
||||||
|
Modifier
|
||||||
.padding(
|
.padding(
|
||||||
top = ZcashTheme.dimens.spacingTiny,
|
top = ZcashTheme.dimens.spacingTiny,
|
||||||
bottom = ZcashTheme.dimens.spacingTiny,
|
bottom = ZcashTheme.dimens.spacingTiny,
|
||||||
|
|
|
@ -85,10 +85,12 @@ fun ChipOnSurface(
|
||||||
) {
|
) {
|
||||||
Surface(
|
Surface(
|
||||||
shape = RectangleShape,
|
shape = RectangleShape,
|
||||||
modifier = modifier
|
modifier =
|
||||||
|
modifier
|
||||||
.padding(horizontal = ZcashTheme.dimens.spacingTiny)
|
.padding(horizontal = ZcashTheme.dimens.spacingTiny)
|
||||||
.border(
|
.border(
|
||||||
border = BorderStroke(
|
border =
|
||||||
|
BorderStroke(
|
||||||
width = ZcashTheme.dimens.chipStroke,
|
width = ZcashTheme.dimens.chipStroke,
|
||||||
color = ZcashTheme.colors.layoutStroke
|
color = ZcashTheme.colors.layoutStroke
|
||||||
)
|
)
|
||||||
|
@ -100,7 +102,8 @@ fun ChipOnSurface(
|
||||||
text = text,
|
text = text,
|
||||||
style = MaterialTheme.typography.bodyLarge,
|
style = MaterialTheme.typography.bodyLarge,
|
||||||
color = MaterialTheme.colorScheme.onSecondary,
|
color = MaterialTheme.colorScheme.onSecondary,
|
||||||
modifier = Modifier
|
modifier =
|
||||||
|
Modifier
|
||||||
.padding(
|
.padding(
|
||||||
vertical = ZcashTheme.dimens.spacingSmall,
|
vertical = ZcashTheme.dimens.spacingSmall,
|
||||||
horizontal = ZcashTheme.dimens.spacingDefault
|
horizontal = ZcashTheme.dimens.spacingDefault
|
||||||
|
|
|
@ -48,11 +48,13 @@ fun ChipGrid(
|
||||||
horizontalArrangement = Arrangement.Center
|
horizontalArrangement = Arrangement.Center
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier =
|
||||||
|
Modifier
|
||||||
.wrapContentWidth()
|
.wrapContentWidth()
|
||||||
.clickable(
|
.clickable(
|
||||||
interactionSource = interactionSource,
|
interactionSource = interactionSource,
|
||||||
indication = null, // Disable ripple
|
// Disable ripple
|
||||||
|
indication = null,
|
||||||
onClick = onGridClick
|
onClick = onGridClick
|
||||||
)
|
)
|
||||||
.testTag(CommonTag.CHIP_LAYOUT)
|
.testTag(CommonTag.CHIP_LAYOUT)
|
||||||
|
|
|
@ -9,10 +9,14 @@ import androidx.compose.ui.graphics.RectangleShape
|
||||||
import co.electriccoin.zcash.ui.design.theme.ZcashTheme
|
import co.electriccoin.zcash.ui.design.theme.ZcashTheme
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun GradientSurface(modifier: Modifier = Modifier, content: @Composable () -> Unit) {
|
fun GradientSurface(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
content: @Composable () -> Unit
|
||||||
|
) {
|
||||||
Surface(
|
Surface(
|
||||||
color = Color.Transparent,
|
color = Color.Transparent,
|
||||||
modifier = modifier
|
modifier =
|
||||||
|
modifier
|
||||||
.background(ZcashTheme.colors.surfaceGradient()),
|
.background(ZcashTheme.colors.surfaceGradient()),
|
||||||
shape = RectangleShape,
|
shape = RectangleShape,
|
||||||
content = content
|
content = content
|
||||||
|
|
|
@ -15,7 +15,10 @@ import kotlinx.coroutines.flow.StateFlow
|
||||||
* for automated tests.
|
* for automated tests.
|
||||||
*/
|
*/
|
||||||
@Composable
|
@Composable
|
||||||
fun Override(configurationOverrideFlow: StateFlow<ConfigurationOverride?>, content: @Composable () -> Unit) {
|
fun Override(
|
||||||
|
configurationOverrideFlow: StateFlow<ConfigurationOverride?>,
|
||||||
|
content: @Composable () -> Unit
|
||||||
|
) {
|
||||||
val configurationOverride = configurationOverrideFlow.collectAsState().value
|
val configurationOverride = configurationOverrideFlow.collectAsState().value
|
||||||
|
|
||||||
if (null == configurationOverride) {
|
if (null == configurationOverride) {
|
||||||
|
@ -23,7 +26,8 @@ fun Override(configurationOverrideFlow: StateFlow<ConfigurationOverride?>, conte
|
||||||
} else {
|
} else {
|
||||||
val configuration = configurationOverride.newConfiguration(LocalConfiguration.current)
|
val configuration = configurationOverride.newConfiguration(LocalConfiguration.current)
|
||||||
|
|
||||||
val contextWrapper = run {
|
val contextWrapper =
|
||||||
|
run {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
object : ContextThemeWrapper() {
|
object : ContextThemeWrapper() {
|
||||||
init {
|
init {
|
||||||
|
@ -43,7 +47,8 @@ fun Override(configurationOverrideFlow: StateFlow<ConfigurationOverride?>, conte
|
||||||
}
|
}
|
||||||
|
|
||||||
data class ConfigurationOverride(val uiMode: UiMode?, val locale: LocaleList?) {
|
data class ConfigurationOverride(val uiMode: UiMode?, val locale: LocaleList?) {
|
||||||
fun newConfiguration(fromConfiguration: Configuration) = Configuration(fromConfiguration).apply {
|
fun newConfiguration(fromConfiguration: Configuration) =
|
||||||
|
Configuration(fromConfiguration).apply {
|
||||||
this@ConfigurationOverride.uiMode?.let {
|
this@ConfigurationOverride.uiMode?.let {
|
||||||
uiMode = (uiMode and Configuration.UI_MODE_NIGHT_MASK.inv()) or it.flag()
|
uiMode = (uiMode and Configuration.UI_MODE_NIGHT_MASK.inv()) or it.flag()
|
||||||
}
|
}
|
||||||
|
@ -59,7 +64,8 @@ enum class UiMode {
|
||||||
Dark
|
Dark
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun UiMode.flag() = when (this) {
|
private fun UiMode.flag() =
|
||||||
|
when (this) {
|
||||||
UiMode.Light -> Configuration.UI_MODE_NIGHT_NO
|
UiMode.Light -> Configuration.UI_MODE_NIGHT_NO
|
||||||
UiMode.Dark -> Configuration.UI_MODE_NIGHT_YES
|
UiMode.Dark -> Configuration.UI_MODE_NIGHT_YES
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,10 +24,12 @@ fun SwitchWithLabel(
|
||||||
val interactionSource = remember { MutableInteractionSource() }
|
val interactionSource = remember { MutableInteractionSource() }
|
||||||
|
|
||||||
ConstraintLayout(
|
ConstraintLayout(
|
||||||
modifier = modifier
|
modifier =
|
||||||
|
modifier
|
||||||
.clickable(
|
.clickable(
|
||||||
interactionSource = interactionSource,
|
interactionSource = interactionSource,
|
||||||
indication = null, // disable ripple
|
// disable ripple
|
||||||
|
indication = null,
|
||||||
role = Role.Switch,
|
role = Role.Switch,
|
||||||
onClick = { onStateChange(!state) }
|
onClick = { onStateChange(!state) }
|
||||||
)
|
)
|
||||||
|
@ -36,7 +38,8 @@ fun SwitchWithLabel(
|
||||||
val (text, spacer, switchButton) = createRefs()
|
val (text, spacer, switchButton) = createRefs()
|
||||||
Body(
|
Body(
|
||||||
text = label,
|
text = label,
|
||||||
modifier = Modifier.constrainAs(text) {
|
modifier =
|
||||||
|
Modifier.constrainAs(text) {
|
||||||
top.linkTo(parent.top)
|
top.linkTo(parent.top)
|
||||||
bottom.linkTo(parent.top)
|
bottom.linkTo(parent.top)
|
||||||
start.linkTo(parent.start)
|
start.linkTo(parent.start)
|
||||||
|
@ -45,7 +48,8 @@ fun SwitchWithLabel(
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
Spacer(
|
Spacer(
|
||||||
modifier = Modifier
|
modifier =
|
||||||
|
Modifier
|
||||||
.width(ZcashTheme.dimens.spacingDefault)
|
.width(ZcashTheme.dimens.spacingDefault)
|
||||||
.constrainAs(spacer) {
|
.constrainAs(spacer) {
|
||||||
top.linkTo(parent.top)
|
top.linkTo(parent.top)
|
||||||
|
@ -59,7 +63,8 @@ fun SwitchWithLabel(
|
||||||
onCheckedChange = {
|
onCheckedChange = {
|
||||||
onStateChange(it)
|
onStateChange(it)
|
||||||
},
|
},
|
||||||
modifier = Modifier.constrainAs(switchButton) {
|
modifier =
|
||||||
|
Modifier.constrainAs(switchButton) {
|
||||||
top.linkTo(parent.top)
|
top.linkTo(parent.top)
|
||||||
bottom.linkTo(parent.top)
|
bottom.linkTo(parent.top)
|
||||||
start.linkTo(spacer.end)
|
start.linkTo(spacer.end)
|
||||||
|
|
|
@ -147,14 +147,16 @@ fun Reference(
|
||||||
onClick: () -> Unit
|
onClick: () -> Unit
|
||||||
) {
|
) {
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier =
|
||||||
|
Modifier
|
||||||
.wrapContentSize()
|
.wrapContentSize()
|
||||||
.clip(RoundedCornerShape(ZcashTheme.dimens.topAppBarActionRippleCorner))
|
.clip(RoundedCornerShape(ZcashTheme.dimens.topAppBarActionRippleCorner))
|
||||||
.clickable { onClick() }
|
.clickable { onClick() }
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = text,
|
text = text,
|
||||||
style = MaterialTheme.typography.bodyLarge
|
style =
|
||||||
|
MaterialTheme.typography.bodyLarge
|
||||||
.merge(
|
.merge(
|
||||||
TextStyle(
|
TextStyle(
|
||||||
color = ZcashTheme.colors.reference,
|
color = ZcashTheme.colors.reference,
|
||||||
|
|
|
@ -27,7 +27,8 @@ fun FormTextField(
|
||||||
leadingIcon: @Composable (() -> Unit)? = null,
|
leadingIcon: @Composable (() -> Unit)? = null,
|
||||||
trailingIcon: @Composable (() -> Unit)? = null,
|
trailingIcon: @Composable (() -> Unit)? = null,
|
||||||
keyboardOptions: KeyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Text),
|
keyboardOptions: KeyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Text),
|
||||||
colors: TextFieldColors = TextFieldDefaults.colors(
|
colors: TextFieldColors =
|
||||||
|
TextFieldDefaults.colors(
|
||||||
focusedContainerColor = Color.Transparent,
|
focusedContainerColor = Color.Transparent,
|
||||||
unfocusedContainerColor = Color.Transparent,
|
unfocusedContainerColor = Color.Transparent,
|
||||||
disabledContainerColor = Color.Transparent,
|
disabledContainerColor = Color.Transparent,
|
||||||
|
@ -35,7 +36,8 @@ fun FormTextField(
|
||||||
),
|
),
|
||||||
keyboardActions: KeyboardActions = KeyboardActions.Default,
|
keyboardActions: KeyboardActions = KeyboardActions.Default,
|
||||||
shape: Shape = TextFieldDefaults.shape,
|
shape: Shape = TextFieldDefaults.shape,
|
||||||
withBorder: Boolean = true, // To enable border around the TextField
|
// To enable border around the TextField
|
||||||
|
withBorder: Boolean = true,
|
||||||
) {
|
) {
|
||||||
TextField(
|
TextField(
|
||||||
value = value,
|
value = value,
|
||||||
|
@ -44,7 +46,8 @@ fun FormTextField(
|
||||||
textStyle = textStyle,
|
textStyle = textStyle,
|
||||||
keyboardOptions = keyboardOptions,
|
keyboardOptions = keyboardOptions,
|
||||||
colors = colors,
|
colors = colors,
|
||||||
modifier = modifier.then(
|
modifier =
|
||||||
|
modifier.then(
|
||||||
if (withBorder) {
|
if (withBorder) {
|
||||||
modifier.border(width = 1.dp, color = MaterialTheme.colorScheme.primary)
|
modifier.border(width = 1.dp, color = MaterialTheme.colorScheme.primary)
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -181,7 +181,8 @@ private fun TopBarOneVisibleActionMenuExample(
|
||||||
text = "Action 1",
|
text = "Action 1",
|
||||||
onClick = actionCallback,
|
onClick = actionCallback,
|
||||||
textAlign = TextAlign.Center,
|
textAlign = TextAlign.Center,
|
||||||
modifier = modifier.then(
|
modifier =
|
||||||
|
modifier.then(
|
||||||
Modifier.padding(all = ZcashTheme.dimens.spacingDefault)
|
Modifier.padding(all = ZcashTheme.dimens.spacingDefault)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
@ -218,7 +219,8 @@ fun SmallTopAppBar(
|
||||||
navigationIcon = {
|
navigationIcon = {
|
||||||
backText?.let {
|
backText?.let {
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier =
|
||||||
|
Modifier
|
||||||
.wrapContentSize()
|
.wrapContentSize()
|
||||||
.clip(RoundedCornerShape(ZcashTheme.dimens.topAppBarActionRippleCorner))
|
.clip(RoundedCornerShape(ZcashTheme.dimens.topAppBarActionRippleCorner))
|
||||||
.clickable { onBack?.run { onBack() } }
|
.clickable { onBack?.run { onBack() } }
|
||||||
|
|
|
@ -18,30 +18,23 @@ data class Dimens(
|
||||||
val spacingLarge: Dp,
|
val spacingLarge: Dp,
|
||||||
val spacingXlarge: Dp,
|
val spacingXlarge: Dp,
|
||||||
val spacingHuge: Dp,
|
val spacingHuge: Dp,
|
||||||
|
|
||||||
// List of custom spacings:
|
// List of custom spacings:
|
||||||
|
|
||||||
// Button:
|
// Button:
|
||||||
val buttonShadowOffsetX: Dp,
|
val buttonShadowOffsetX: Dp,
|
||||||
val buttonShadowOffsetY: Dp,
|
val buttonShadowOffsetY: Dp,
|
||||||
val buttonShadowSpread: Dp,
|
val buttonShadowSpread: Dp,
|
||||||
val buttonWidth: Dp,
|
val buttonWidth: Dp,
|
||||||
val buttonHeight: Dp,
|
val buttonHeight: Dp,
|
||||||
|
|
||||||
// Chip
|
// Chip
|
||||||
val chipShadowElevation: Dp,
|
val chipShadowElevation: Dp,
|
||||||
val chipStroke: Dp,
|
val chipStroke: Dp,
|
||||||
|
|
||||||
// TopAppBar:
|
// TopAppBar:
|
||||||
val topAppBarZcashLogoHeight: Dp,
|
val topAppBarZcashLogoHeight: Dp,
|
||||||
val topAppBarActionRippleCorner: Dp,
|
val topAppBarActionRippleCorner: Dp,
|
||||||
|
|
||||||
// TextField:
|
// TextField:
|
||||||
val textFieldDefaultHeight: Dp,
|
val textFieldDefaultHeight: Dp,
|
||||||
|
|
||||||
// Any Layout:
|
// Any Layout:
|
||||||
val layoutStroke: Dp,
|
val layoutStroke: Dp,
|
||||||
|
|
||||||
// Screen custom spacings:
|
// Screen custom spacings:
|
||||||
val inScreenZcashLogoHeight: Dp,
|
val inScreenZcashLogoHeight: Dp,
|
||||||
val inScreenZcashLogoWidth: Dp,
|
val inScreenZcashLogoWidth: Dp,
|
||||||
|
@ -49,7 +42,8 @@ data class Dimens(
|
||||||
val screenHorizontalSpacing: Dp,
|
val screenHorizontalSpacing: Dp,
|
||||||
)
|
)
|
||||||
|
|
||||||
private val defaultDimens = Dimens(
|
private val defaultDimens =
|
||||||
|
Dimens(
|
||||||
spacingNone = 0.dp,
|
spacingNone = 0.dp,
|
||||||
spacingXtiny = 2.dp,
|
spacingXtiny = 2.dp,
|
||||||
spacingTiny = 4.dp,
|
spacingTiny = 4.dp,
|
||||||
|
@ -73,11 +67,11 @@ private val defaultDimens = Dimens(
|
||||||
inScreenZcashLogoWidth = 60.dp,
|
inScreenZcashLogoWidth = 60.dp,
|
||||||
inScreenZcashTextLogoHeight = 30.dp,
|
inScreenZcashTextLogoHeight = 30.dp,
|
||||||
screenHorizontalSpacing = 64.dp,
|
screenHorizontalSpacing = 64.dp,
|
||||||
)
|
)
|
||||||
|
|
||||||
private val normalDimens = defaultDimens
|
private val normalDimens = defaultDimens
|
||||||
|
|
||||||
internal var LocalDimens = staticCompositionLocalOf { defaultDimens }
|
internal var localDimens = staticCompositionLocalOf { defaultDimens }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This is a convenience way on how to provide device specification based spacings. We use Configuration from Compose
|
* This is a convenience way on how to provide device specification based spacings. We use Configuration from Compose
|
||||||
|
@ -119,7 +113,7 @@ internal var LocalDimens = staticCompositionLocalOf { defaultDimens }
|
||||||
* - rounded/normal screen shape
|
* - rounded/normal screen shape
|
||||||
*/
|
*/
|
||||||
@Composable
|
@Composable
|
||||||
internal fun ProvideDimens(content: @Composable () -> Unit,) {
|
internal fun ProvideDimens(content: @Composable () -> Unit) {
|
||||||
val resultDimens = normalDimens
|
val resultDimens = normalDimens
|
||||||
CompositionLocalProvider(LocalDimens provides resultDimens, content = content)
|
CompositionLocalProvider(localDimens provides resultDimens, content = content)
|
||||||
}
|
}
|
||||||
|
|
|
@ -37,8 +37,10 @@ data class ExtendedColors(
|
||||||
val welcomeAnimationColor: Color,
|
val welcomeAnimationColor: Color,
|
||||||
) {
|
) {
|
||||||
@Composable
|
@Composable
|
||||||
fun surfaceGradient() = Brush.verticalGradient(
|
fun surfaceGradient() =
|
||||||
colors = listOf(
|
Brush.verticalGradient(
|
||||||
|
colors =
|
||||||
|
listOf(
|
||||||
MaterialTheme.colorScheme.surface,
|
MaterialTheme.colorScheme.surface,
|
||||||
ZcashTheme.colors.surfaceEnd
|
ZcashTheme.colors.surfaceEnd
|
||||||
)
|
)
|
||||||
|
|
|
@ -31,13 +31,15 @@ fun ZcashTheme(
|
||||||
// IS_APP_DARK_MODE_ENABLED, whether the device's system dark mode is on or off.
|
// IS_APP_DARK_MODE_ENABLED, whether the device's system dark mode is on or off.
|
||||||
val useDarkMode = forceDarkMode || (BuildConfig.IS_APP_DARK_MODE_ENABLED && isSystemInDarkTheme())
|
val useDarkMode = forceDarkMode || (BuildConfig.IS_APP_DARK_MODE_ENABLED && isSystemInDarkTheme())
|
||||||
|
|
||||||
val baseColors = if (useDarkMode) {
|
val baseColors =
|
||||||
|
if (useDarkMode) {
|
||||||
DarkColorPalette
|
DarkColorPalette
|
||||||
} else {
|
} else {
|
||||||
LightColorPalette
|
LightColorPalette
|
||||||
}
|
}
|
||||||
|
|
||||||
val extendedColors = if (useDarkMode) {
|
val extendedColors =
|
||||||
|
if (useDarkMode) {
|
||||||
DarkExtendedColorPalette
|
DarkExtendedColorPalette
|
||||||
} else {
|
} else {
|
||||||
LightExtendedColorPalette
|
LightExtendedColorPalette
|
||||||
|
@ -72,5 +74,5 @@ object ZcashTheme {
|
||||||
// TODO [#808]: https://github.com/Electric-Coin-Company/zashi-android/issues/808
|
// TODO [#808]: https://github.com/Electric-Coin-Company/zashi-android/issues/808
|
||||||
val dimens: Dimens
|
val dimens: Dimens
|
||||||
@Composable
|
@Composable
|
||||||
get() = LocalDimens.current
|
get() = localDimens.current
|
||||||
}
|
}
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue