[#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
|
||||
|
||||
class AndroidApiTest {
|
||||
|
||||
@Test
|
||||
@SmallTest
|
||||
fun checkTargetApi() {
|
||||
|
|
|
@ -9,7 +9,6 @@ import kotlinx.coroutines.launch
|
|||
|
||||
@Suppress("unused")
|
||||
class ZcashApplication : CoroutineApplication() {
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
|
||||
|
|
|
@ -5,7 +5,7 @@ plugins {
|
|||
val ktlint by configurations.creating
|
||||
|
||||
dependencies {
|
||||
ktlint("com.pinterest:ktlint:${project.property("KTLINT_VERSION")}") {
|
||||
ktlint("com.pinterest.ktlint:ktlint-cli:${project.property("KTLINT_VERSION")}") {
|
||||
attributes {
|
||||
attribute(Bundling.BUNDLING_ATTRIBUTE, objects.named<Bundling>(Bundling.EXTERNAL))
|
||||
}
|
||||
|
|
|
@ -7,7 +7,6 @@ import kotlinx.coroutines.flow.Flow
|
|||
* Provides a remote config implementation.
|
||||
*/
|
||||
interface ConfigurationProvider {
|
||||
|
||||
/**
|
||||
* @return The configuration if it has been loaded already. If not loaded, returns an empty configuration.
|
||||
*/
|
||||
|
|
|
@ -40,19 +40,28 @@ private data class MergingConfiguration(private val configurations: PersistentLi
|
|||
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 it.getBoolean(key, defaultValue)
|
||||
} ?: defaultValue
|
||||
}
|
||||
|
||||
override fun getInt(key: ConfigKey, defaultValue: Int): Int {
|
||||
override fun getInt(
|
||||
key: ConfigKey,
|
||||
defaultValue: Int
|
||||
): Int {
|
||||
return configurations.firstWithKey(key)?.let {
|
||||
return it.getInt(key, defaultValue)
|
||||
} ?: defaultValue
|
||||
}
|
||||
|
||||
override fun getString(key: ConfigKey, defaultValue: String): String {
|
||||
override fun getString(
|
||||
key: ConfigKey,
|
||||
defaultValue: String
|
||||
): String {
|
||||
return configurations.firstWithKey(key)?.let {
|
||||
return it.getString(key, defaultValue)
|
||||
} ?: defaultValue
|
||||
|
|
|
@ -6,7 +6,5 @@ data class BooleanConfigurationEntry(
|
|||
override val key: ConfigKey,
|
||||
private val defaultValue: Boolean
|
||||
) : DefaultEntry<Boolean> {
|
||||
|
||||
override fun getValue(configuration: Configuration) =
|
||||
configuration.getBoolean(key, defaultValue)
|
||||
override fun getValue(configuration: Configuration) = configuration.getBoolean(key, defaultValue)
|
||||
}
|
||||
|
|
|
@ -8,16 +8,16 @@ import co.electriccoin.zcash.configuration.model.map.Configuration
|
|||
* variation in default value. Clients define the key and default value together, rather than just
|
||||
* the key.
|
||||
*/
|
||||
/*
|
||||
* API note: the default value is not available through the public interface in order to prevent
|
||||
* clients from accidentally using the default value instead of the configuration value.
|
||||
*
|
||||
* Implementation note: although primitives would be nice, Objects don't increase memory usage much.
|
||||
* The autoboxing cache solves Booleans, and Strings are already objects, so that just leaves Integers.
|
||||
* Overall the number of Integer configuration entries is expected to be low compared to Booleans,
|
||||
* and perhaps many Integer values will also fit within the autoboxing cache.
|
||||
*/
|
||||
interface DefaultEntry<T> {
|
||||
/*
|
||||
* API note: the default value is not available through the public interface in order to prevent
|
||||
* clients from accidentally using the default value instead of the configuration value.
|
||||
*
|
||||
* Implementation note: although primitives would be nice, Objects don't increase memory usage much.
|
||||
* The autoboxing cache solves Booleans, and Strings are already objects, so that just leaves Integers.
|
||||
* Overall the number of Integer configuration entries is expected to be low compared to Booleans,
|
||||
* and perhaps many Integer values will also fit within the autoboxing cache.
|
||||
*/
|
||||
|
||||
val key: ConfigKey
|
||||
|
||||
|
|
|
@ -6,6 +6,5 @@ data class IntegerConfigurationEntry(
|
|||
override val key: ConfigKey,
|
||||
private val defaultValue: Int
|
||||
) : DefaultEntry<Int> {
|
||||
|
||||
override fun getValue(configuration: Configuration) = configuration.getInt(key, defaultValue)
|
||||
}
|
||||
|
|
|
@ -6,6 +6,5 @@ data class StringConfigurationEntry(
|
|||
override val key: ConfigKey,
|
||||
private val defaultValue: String
|
||||
) : DefaultEntry<String> {
|
||||
|
||||
override fun getValue(configuration: Configuration) = configuration.getString(key, defaultValue)
|
||||
}
|
||||
|
|
|
@ -26,7 +26,10 @@ interface Configuration {
|
|||
* be returned if type coercion fails.
|
||||
* @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.
|
||||
|
@ -35,7 +38,10 @@ interface Configuration {
|
|||
* be returned if type coercion fails.
|
||||
* @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.
|
||||
|
@ -44,5 +50,8 @@ interface Configuration {
|
|||
* be returned if type coercion fails.
|
||||
* @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>,
|
||||
override val updatedAt: Instant?
|
||||
) : Configuration {
|
||||
|
||||
override fun getBoolean(
|
||||
key: ConfigKey,
|
||||
defaultValue: Boolean
|
||||
) = configurationMapping[key.key]?.let {
|
||||
try {
|
||||
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
|
||||
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 {
|
||||
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
|
||||
defaultValue
|
||||
}
|
||||
} ?: defaultValue
|
||||
|
||||
override fun getString(key: ConfigKey, defaultValue: String) =
|
||||
configurationMapping.getOrElse(key.key) { defaultValue }
|
||||
override fun getString(
|
||||
key: ConfigKey,
|
||||
defaultValue: String
|
||||
) = configurationMapping.getOrElse(key.key) { defaultValue }
|
||||
|
||||
override fun hasKey(key: ConfigKey) = configurationMapping.containsKey(key.key)
|
||||
}
|
||||
|
|
|
@ -18,72 +18,98 @@ import kotlin.test.assertTrue
|
|||
class MergingConfigurationProviderTest {
|
||||
@Test
|
||||
fun peek_ordering() {
|
||||
val configurationProvider = MergingConfigurationProvider(
|
||||
persistentListOf(
|
||||
MockConfigurationProvider(
|
||||
StringConfiguration(persistentMapOf(BooleanDefaultEntryFixture.KEY.key to true.toString()), null)
|
||||
),
|
||||
MockConfigurationProvider(
|
||||
StringConfiguration(persistentMapOf(BooleanDefaultEntryFixture.KEY.key to false.toString()), null)
|
||||
val configurationProvider =
|
||||
MergingConfigurationProvider(
|
||||
persistentListOf(
|
||||
MockConfigurationProvider(
|
||||
StringConfiguration(
|
||||
persistentMapOf(BooleanDefaultEntryFixture.KEY.key to true.toString()),
|
||||
null
|
||||
)
|
||||
),
|
||||
MockConfigurationProvider(
|
||||
StringConfiguration(
|
||||
persistentMapOf(BooleanDefaultEntryFixture.KEY.key to false.toString()),
|
||||
null
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
assertTrue(BooleanDefaultEntryFixture.newTrueEntry().getValue(configurationProvider.peekConfiguration()))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun getFlow_ordering() = runTest {
|
||||
val configurationProvider = MergingConfigurationProvider(
|
||||
persistentListOf(
|
||||
MockConfigurationProvider(
|
||||
StringConfiguration(persistentMapOf(BooleanDefaultEntryFixture.KEY.key to true.toString()), null)
|
||||
),
|
||||
MockConfigurationProvider(
|
||||
StringConfiguration(persistentMapOf(BooleanDefaultEntryFixture.KEY.key to false.toString()), null)
|
||||
fun getFlow_ordering() =
|
||||
runTest {
|
||||
val configurationProvider =
|
||||
MergingConfigurationProvider(
|
||||
persistentListOf(
|
||||
MockConfigurationProvider(
|
||||
StringConfiguration(
|
||||
persistentMapOf(BooleanDefaultEntryFixture.KEY.key to true.toString()),
|
||||
null
|
||||
)
|
||||
),
|
||||
MockConfigurationProvider(
|
||||
StringConfiguration(
|
||||
persistentMapOf(BooleanDefaultEntryFixture.KEY.key to false.toString()),
|
||||
null
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
assertTrue(
|
||||
BooleanDefaultEntryFixture.newTrueEntry().getValue(configurationProvider.getConfigurationFlow().first())
|
||||
)
|
||||
}
|
||||
assertTrue(
|
||||
BooleanDefaultEntryFixture.newTrueEntry().getValue(configurationProvider.getConfigurationFlow().first())
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun getFlow_empty() = runTest {
|
||||
val configurationProvider = MergingConfigurationProvider(
|
||||
emptyList<ConfigurationProvider>().toPersistentList()
|
||||
)
|
||||
fun getFlow_empty() =
|
||||
runTest {
|
||||
val configurationProvider =
|
||||
MergingConfigurationProvider(
|
||||
emptyList<ConfigurationProvider>().toPersistentList()
|
||||
)
|
||||
|
||||
val firstMergedConfiguration = configurationProvider.getConfigurationFlow().first()
|
||||
val firstMergedConfiguration = configurationProvider.getConfigurationFlow().first()
|
||||
|
||||
assertTrue(BooleanDefaultEntryFixture.newTrueEntry().getValue(firstMergedConfiguration))
|
||||
}
|
||||
assertTrue(BooleanDefaultEntryFixture.newTrueEntry().getValue(firstMergedConfiguration))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun getUpdatedAt_newest() = runTest {
|
||||
val older = "2023-01-15T08:38:45.415Z".toInstant()
|
||||
val newer = "2023-01-17T08:38:45.415Z".toInstant()
|
||||
fun getUpdatedAt_newest() =
|
||||
runTest {
|
||||
val older = "2023-01-15T08:38:45.415Z".toInstant()
|
||||
val newer = "2023-01-17T08:38:45.415Z".toInstant()
|
||||
|
||||
val configurationProvider = MergingConfigurationProvider(
|
||||
persistentListOf(
|
||||
MockConfigurationProvider(
|
||||
StringConfiguration(persistentMapOf(BooleanDefaultEntryFixture.KEY.key to true.toString()), older)
|
||||
),
|
||||
MockConfigurationProvider(
|
||||
StringConfiguration(persistentMapOf(BooleanDefaultEntryFixture.KEY.key to false.toString()), newer)
|
||||
val configurationProvider =
|
||||
MergingConfigurationProvider(
|
||||
persistentListOf(
|
||||
MockConfigurationProvider(
|
||||
StringConfiguration(
|
||||
persistentMapOf(BooleanDefaultEntryFixture.KEY.key to true.toString()),
|
||||
older
|
||||
)
|
||||
),
|
||||
MockConfigurationProvider(
|
||||
StringConfiguration(
|
||||
persistentMapOf(
|
||||
BooleanDefaultEntryFixture.KEY.key to false.toString()
|
||||
),
|
||||
newer
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
val updatedAt = configurationProvider.getConfigurationFlow().first().updatedAt
|
||||
assertEquals(newer, updatedAt)
|
||||
}
|
||||
val updatedAt = configurationProvider.getConfigurationFlow().first().updatedAt
|
||||
assertEquals(newer, updatedAt)
|
||||
}
|
||||
}
|
||||
|
||||
private class MockConfigurationProvider(private val configuration: Configuration) : ConfigurationProvider {
|
||||
|
||||
override fun peekConfiguration(): Configuration {
|
||||
return configuration
|
||||
}
|
||||
|
|
|
@ -11,7 +11,6 @@ import kotlinx.datetime.Instant
|
|||
* though, making the initial mapping thread-safe.
|
||||
*/
|
||||
class MockConfiguration(private val configurationMapping: Map<String, String> = emptyMap()) : Configuration {
|
||||
|
||||
override val updatedAt: Instant? = null
|
||||
|
||||
override fun getBoolean(
|
||||
|
@ -20,23 +19,32 @@ class MockConfiguration(private val configurationMapping: Map<String, String> =
|
|||
) = configurationMapping[key.key]?.let {
|
||||
try {
|
||||
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
|
||||
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 {
|
||||
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
|
||||
defaultValue
|
||||
}
|
||||
} ?: defaultValue
|
||||
|
||||
override fun getString(key: ConfigKey, defaultValue: String) =
|
||||
configurationMapping.getOrElse(key.key) { defaultValue }
|
||||
override fun getString(
|
||||
key: ConfigKey,
|
||||
defaultValue: String
|
||||
) = configurationMapping.getOrElse(key.key) { defaultValue }
|
||||
|
||||
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
|
||||
|
||||
object BooleanDefaultEntryFixture {
|
||||
|
||||
val KEY = ConfigKey("some_boolean_key") // $NON-NLS
|
||||
|
||||
fun newTrueEntry() = BooleanConfigurationEntry(KEY, true)
|
||||
|
|
|
@ -6,5 +6,9 @@ import co.electriccoin.zcash.configuration.model.entry.IntegerConfigurationEntry
|
|||
object IntegerDefaultEntryFixture {
|
||||
val KEY = ConfigKey("some_string_key") // $NON-NLS
|
||||
const val DEFAULT_VALUE = 123
|
||||
fun newEntry(key: ConfigKey = KEY, value: Int = DEFAULT_VALUE) = IntegerConfigurationEntry(key, value)
|
||||
|
||||
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 {
|
||||
val KEY = ConfigKey("some_string_key") // $NON-NLS
|
||||
const val DEFAULT_VALUE = "some_default_value" // $NON-NLS
|
||||
fun newEntryEntry(key: ConfigKey = KEY, value: String = DEFAULT_VALUE) = StringConfigurationEntry(key, value)
|
||||
|
||||
fun newEntryEntry(
|
||||
key: ConfigKey = KEY,
|
||||
value: String = DEFAULT_VALUE
|
||||
) = StringConfigurationEntry(key, value)
|
||||
}
|
||||
|
|
|
@ -8,25 +8,28 @@ import co.electriccoin.zcash.spackle.LazyWithArgument
|
|||
import kotlinx.collections.immutable.toPersistentList
|
||||
|
||||
object AndroidConfigurationFactory {
|
||||
|
||||
private val instance = LazyWithArgument<Context, ConfigurationProvider> { context ->
|
||||
new(context)
|
||||
}
|
||||
private val instance =
|
||||
LazyWithArgument<Context, ConfigurationProvider> { context ->
|
||||
new(context)
|
||||
}
|
||||
|
||||
fun getInstance(context: Context): ConfigurationProvider = instance.getInstance(context)
|
||||
|
||||
// Context will be needed for most cloud providers, e.g. to integrate with Firebase or other
|
||||
// remote configuration providers.
|
||||
private fun new(@Suppress("UNUSED_PARAMETER") context: Context): ConfigurationProvider {
|
||||
val configurationProviders = buildList<ConfigurationProvider> {
|
||||
// For ordering, ensure the IntentConfigurationProvider is first so that it can
|
||||
// override any other configuration providers.
|
||||
if (BuildConfig.DEBUG) {
|
||||
add(IntentConfigurationProvider)
|
||||
}
|
||||
private fun new(
|
||||
@Suppress("UNUSED_PARAMETER") context: Context
|
||||
): ConfigurationProvider {
|
||||
val configurationProviders =
|
||||
buildList<ConfigurationProvider> {
|
||||
// For ordering, ensure the IntentConfigurationProvider is first so that it can
|
||||
// override any other configuration providers.
|
||||
if (BuildConfig.DEBUG) {
|
||||
add(IntentConfigurationProvider)
|
||||
}
|
||||
|
||||
// In the future, add a third party cloud-based configuration provider
|
||||
}
|
||||
// In the future, add a third party cloud-based configuration provider
|
||||
}
|
||||
|
||||
return MergingConfigurationProvider(configurationProviders.toPersistentList())
|
||||
}
|
||||
|
|
|
@ -8,7 +8,6 @@ import kotlinx.coroutines.flow.Flow
|
|||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
|
||||
internal object IntentConfigurationProvider : ConfigurationProvider {
|
||||
|
||||
private val configurationStateFlow = MutableStateFlow(StringConfiguration(persistentMapOf(), null))
|
||||
|
||||
override fun peekConfiguration() = configurationStateFlow.value
|
||||
|
|
|
@ -8,18 +8,22 @@ import kotlinx.collections.immutable.toPersistentMap
|
|||
import kotlinx.datetime.Clock
|
||||
|
||||
class IntentConfigurationReceiver : BroadcastReceiver() {
|
||||
override fun onReceive(context: Context?, intent: Intent?) {
|
||||
override fun onReceive(
|
||||
context: Context?,
|
||||
intent: Intent?
|
||||
) {
|
||||
intent?.defuse()?.let {
|
||||
val key = it.getStringExtra(ConfigurationIntent.EXTRA_STRING_KEY)
|
||||
val value = it.getStringExtra(ConfigurationIntent.EXTRA_STRING_VALUE)
|
||||
|
||||
if (null != key) {
|
||||
val existingConfiguration = IntentConfigurationProvider.peekConfiguration().configurationMapping
|
||||
val newConfiguration = if (null == value) {
|
||||
existingConfiguration.remove(key)
|
||||
} else {
|
||||
existingConfiguration + (key to value)
|
||||
}
|
||||
val newConfiguration =
|
||||
if (null == value) {
|
||||
existingConfiguration.remove(key)
|
||||
} else {
|
||||
existingConfiguration + (key to value)
|
||||
}
|
||||
|
||||
IntentConfigurationProvider.setConfiguration(
|
||||
StringConfiguration(newConfiguration.toPersistentMap(), Clock.System.now())
|
||||
|
@ -34,7 +38,9 @@ private fun Intent.defuse(): Intent? {
|
|||
return try {
|
||||
extras?.containsKey(null)
|
||||
this
|
||||
} catch (@Suppress("SwallowedException", "TooGenericExceptionCaught") e: Exception) {
|
||||
} catch (
|
||||
@Suppress("SwallowedException", "TooGenericExceptionCaught") e: Exception
|
||||
) {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,7 +9,6 @@ import java.util.concurrent.CountDownLatch
|
|||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
|
||||
class AndroidUncaughtExceptionHandlerTest {
|
||||
|
||||
@Test(expected = IllegalStateException::class)
|
||||
fun requires_main_thread() {
|
||||
AndroidUncaughtExceptionHandler.register(ApplicationProvider.getApplicationContext())
|
||||
|
|
|
@ -12,7 +12,6 @@ import org.junit.Assert.assertTrue
|
|||
import org.junit.Test
|
||||
|
||||
class Components {
|
||||
|
||||
@Test
|
||||
@SmallTest
|
||||
fun process_names() {
|
||||
|
@ -26,16 +25,18 @@ class Components {
|
|||
}
|
||||
}
|
||||
|
||||
private fun PackageManager.getProviderInfoCompat(componentName: ComponentName) = if (AndroidApiVersion.isAtLeastT) {
|
||||
getProviderInfo(componentName, PackageManager.ComponentInfoFlags.of(0))
|
||||
} else {
|
||||
@Suppress("Deprecation")
|
||||
getProviderInfo(componentName, 0)
|
||||
}
|
||||
private fun PackageManager.getProviderInfoCompat(componentName: ComponentName) =
|
||||
if (AndroidApiVersion.isAtLeastT) {
|
||||
getProviderInfo(componentName, PackageManager.ComponentInfoFlags.of(0))
|
||||
} else {
|
||||
@Suppress("Deprecation")
|
||||
getProviderInfo(componentName, 0)
|
||||
}
|
||||
|
||||
private fun PackageManager.getReceiverInfoCompat(componentName: ComponentName) = if (AndroidApiVersion.isAtLeastT) {
|
||||
getReceiverInfo(componentName, PackageManager.ComponentInfoFlags.of(0))
|
||||
} else {
|
||||
@Suppress("Deprecation")
|
||||
getReceiverInfo(componentName, 0)
|
||||
}
|
||||
private fun PackageManager.getReceiverInfoCompat(componentName: ComponentName) =
|
||||
if (AndroidApiVersion.isAtLeastT) {
|
||||
getReceiverInfo(componentName, PackageManager.ComponentInfoFlags.of(0))
|
||||
} else {
|
||||
@Suppress("Deprecation")
|
||||
getReceiverInfo(componentName, 0)
|
||||
}
|
||||
|
|
|
@ -6,7 +6,6 @@ import org.junit.Assert.assertEquals
|
|||
import org.junit.Test
|
||||
|
||||
class ReportableExceptionTest {
|
||||
|
||||
@Test
|
||||
fun bundle() {
|
||||
val reportableException = ReportableExceptionFixture.new()
|
||||
|
|
|
@ -9,8 +9,9 @@ import java.io.File
|
|||
|
||||
@Suppress("ReturnCount")
|
||||
suspend fun ExceptionPath.getExceptionDirectory(context: Context): File? {
|
||||
val exceptionDirectory = context.getExternalFilesDirSuspend(null)
|
||||
?.let { File(File(it, ExceptionPath.LOG_DIRECTORY_NAME), ExceptionPath.EXCEPTION_DIRECTORY_NAME) }
|
||||
val exceptionDirectory =
|
||||
context.getExternalFilesDirSuspend(null)
|
||||
?.let { File(File(it, ExceptionPath.LOG_DIRECTORY_NAME), ExceptionPath.EXCEPTION_DIRECTORY_NAME) }
|
||||
|
||||
if (null == exceptionDirectory) {
|
||||
Twig.info { "Unable to get external storage directory; external storage may not be available" }
|
||||
|
@ -27,9 +28,13 @@ suspend fun ExceptionPath.getExceptionDirectory(context: Context): File? {
|
|||
return exceptionDirectory
|
||||
}
|
||||
|
||||
suspend fun ExceptionPath.getExceptionPath(context: Context, exception: ReportableException): File? {
|
||||
val exceptionDirectory = getExceptionDirectory(context)
|
||||
?: return null
|
||||
suspend fun ExceptionPath.getExceptionPath(
|
||||
context: Context,
|
||||
exception: ReportableException
|
||||
): File? {
|
||||
val exceptionDirectory =
|
||||
getExceptionDirectory(context)
|
||||
?: return null
|
||||
|
||||
return File(exceptionDirectory, newExceptionFileName(exception))
|
||||
}
|
||||
|
|
|
@ -11,7 +11,6 @@ import co.electriccoin.zcash.spackle.process.ProcessNameCompat
|
|||
import java.util.Collections
|
||||
|
||||
object GlobalCrashReporter {
|
||||
|
||||
internal const val CRASH_PROCESS_NAME_SUFFIX = ":crash" // $NON-NLS
|
||||
|
||||
private val intrinsicLock = Any()
|
||||
|
@ -33,17 +32,18 @@ object GlobalCrashReporter {
|
|||
|
||||
synchronized(intrinsicLock) {
|
||||
if (registeredCrashReporters == null) {
|
||||
registeredCrashReporters = Collections.synchronizedList(
|
||||
// To prevent a race condition, register the LocalCrashReporter first.
|
||||
// FirebaseCrashReporter does some asynchronous registration internally, while
|
||||
// LocalCrashReporter uses AndroidUncaughtExceptionHandler which needs to read
|
||||
// and write the default UncaughtExceptionHandler. The only way to ensure
|
||||
// interleaving doesn't happen is to register the LocalCrashReporter first.
|
||||
listOfNotNull(
|
||||
LocalCrashReporter.getInstance(context),
|
||||
FirebaseCrashReporter(context),
|
||||
registeredCrashReporters =
|
||||
Collections.synchronizedList(
|
||||
// To prevent a race condition, register the LocalCrashReporter first.
|
||||
// FirebaseCrashReporter does some asynchronous registration internally, while
|
||||
// LocalCrashReporter uses AndroidUncaughtExceptionHandler which needs to read
|
||||
// and write the default UncaughtExceptionHandler. The only way to ensure
|
||||
// interleaving doesn't happen is to register the LocalCrashReporter first.
|
||||
listOfNotNull(
|
||||
LocalCrashReporter.getInstance(context),
|
||||
FirebaseCrashReporter(context),
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -3,7 +3,6 @@ package co.electriccoin.zcash.crash.android.internal
|
|||
import androidx.annotation.AnyThread
|
||||
|
||||
interface CrashReporter {
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
val firebaseApp = FirebaseApp.initializeApp(context)
|
||||
FirebaseAppContainer(firebaseApp)
|
||||
}
|
||||
private suspend fun getFirebaseAppContainer(context: Context): FirebaseAppContainer =
|
||||
withContext(Dispatchers.IO) {
|
||||
val firebaseApp = FirebaseApp.initializeApp(context)
|
||||
FirebaseAppContainer(firebaseApp)
|
||||
}
|
||||
|
||||
private class FirebaseAppContainer(val firebaseApp: FirebaseApp?)
|
||||
|
|
|
@ -25,13 +25,13 @@ import kotlinx.coroutines.async
|
|||
internal class FirebaseCrashReporter(
|
||||
context: Context
|
||||
) : CrashReporter {
|
||||
|
||||
@OptIn(kotlinx.coroutines.DelicateCoroutinesApi::class)
|
||||
private val analyticsScope = CoroutineScope(SupervisorJob() + Dispatchers.Main)
|
||||
|
||||
private val initFirebaseJob: Deferred<CrashReporter?> = analyticsScope.async {
|
||||
FirebaseCrashReporterImpl.getInstance(context)
|
||||
}
|
||||
private val initFirebaseJob: Deferred<CrashReporter?> =
|
||||
analyticsScope.async {
|
||||
FirebaseCrashReporterImpl.getInstance(context)
|
||||
}
|
||||
|
||||
@AnyThread
|
||||
override fun reportCaughtException(exception: Throwable) {
|
||||
|
@ -67,7 +67,6 @@ private class FirebaseCrashReporterImpl(
|
|||
private val firebaseCrashlytics: FirebaseCrashlytics,
|
||||
private val firebaseInstallations: FirebaseInstallations
|
||||
) : CrashReporter {
|
||||
|
||||
@AnyThread
|
||||
override fun reportCaughtException(exception: Throwable) {
|
||||
firebaseCrashlytics.recordException(exception)
|
||||
|
@ -90,30 +89,32 @@ private class FirebaseCrashReporterImpl(
|
|||
* early crashes may be missed. This is a tradeoff we are willing to make in order to avoid
|
||||
* ANRs.
|
||||
*/
|
||||
private val lazyWithArgument = SuspendingLazy<Context, CrashReporter?> {
|
||||
if (it.resources.getBoolean(R.bool.co_electriccoin_zcash_crash_is_firebase_enabled)) {
|
||||
private val lazyWithArgument =
|
||||
SuspendingLazy<Context, CrashReporter?> {
|
||||
if (it.resources.getBoolean(R.bool.co_electriccoin_zcash_crash_is_firebase_enabled)) {
|
||||
|
||||
// Workaround for disk IO on main thread in Firebase initialization
|
||||
val firebaseApp = FirebaseAppCache.getFirebaseApp(it)
|
||||
if (firebaseApp == null) {
|
||||
Twig.warn { "Unable to initialize Crashlytics. FirebaseApp is null" }
|
||||
return@SuspendingLazy null
|
||||
// Workaround for disk IO on main thread in Firebase initialization
|
||||
val firebaseApp = FirebaseAppCache.getFirebaseApp(it)
|
||||
if (firebaseApp == null) {
|
||||
Twig.warn { "Unable to initialize Crashlytics. FirebaseApp is null" }
|
||||
return@SuspendingLazy null
|
||||
}
|
||||
|
||||
val firebaseInstallations = FirebaseInstallations.getInstance(firebaseApp)
|
||||
val firebaseCrashlytics =
|
||||
FirebaseCrashlytics.getInstance().apply {
|
||||
setCustomKey(
|
||||
CrashlyticsUserProperties.IS_TEST,
|
||||
EmulatorWtfUtil.isEmulatorWtf(it) || FirebaseTestLabUtil.isFirebaseTestLab(it)
|
||||
)
|
||||
}
|
||||
|
||||
FirebaseCrashReporterImpl(firebaseCrashlytics, firebaseInstallations)
|
||||
} else {
|
||||
Twig.warn { "Unable to initialize Crashlytics. Configure API keys in the app module" }
|
||||
null
|
||||
}
|
||||
|
||||
val firebaseInstallations = FirebaseInstallations.getInstance(firebaseApp)
|
||||
val firebaseCrashlytics = FirebaseCrashlytics.getInstance().apply {
|
||||
setCustomKey(
|
||||
CrashlyticsUserProperties.IS_TEST,
|
||||
EmulatorWtfUtil.isEmulatorWtf(it) || FirebaseTestLabUtil.isFirebaseTestLab(it)
|
||||
)
|
||||
}
|
||||
|
||||
FirebaseCrashReporterImpl(firebaseCrashlytics, firebaseInstallations)
|
||||
} else {
|
||||
Twig.warn { "Unable to initialize Crashlytics. Configure API keys in the app module" }
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getInstance(context: Context): CrashReporter? {
|
||||
return lazyWithArgument.getInstance(context)
|
||||
|
|
|
@ -8,9 +8,13 @@ import co.electriccoin.zcash.crash.android.getExceptionPath
|
|||
import co.electriccoin.zcash.crash.write
|
||||
|
||||
internal object AndroidExceptionReporter {
|
||||
internal suspend fun reportException(context: Context, reportableException: ReportableException) {
|
||||
val exceptionPath = ExceptionPath.getExceptionPath(context, reportableException)
|
||||
?: return
|
||||
internal suspend fun reportException(
|
||||
context: Context,
|
||||
reportableException: ReportableException
|
||||
) {
|
||||
val exceptionPath =
|
||||
ExceptionPath.getExceptionPath(context, reportableException)
|
||||
?: return
|
||||
|
||||
reportableException.write(exceptionPath)
|
||||
|
||||
|
|
|
@ -13,8 +13,9 @@ internal fun ReportableException.Companion.new(
|
|||
isUncaught: Boolean,
|
||||
clock: Clock = Clock.System
|
||||
): ReportableException {
|
||||
val versionName = context.packageManager.getPackageInfoCompat(context.packageName, 0L).versionName
|
||||
?: "null"
|
||||
val versionName =
|
||||
context.packageManager.getPackageInfoCompat(context.packageName, 0L).versionName
|
||||
?: "null"
|
||||
|
||||
return ReportableException(
|
||||
throwable.javaClass.name,
|
||||
|
@ -25,15 +26,16 @@ internal fun ReportableException.Companion.new(
|
|||
)
|
||||
}
|
||||
|
||||
internal fun ReportableException.toBundle() = Bundle().apply {
|
||||
// Although Exception is Serializable, some Kotlin Coroutines exception classes break this
|
||||
// API contract. Therefore we have to convert to a string here.
|
||||
putSerializable(ReportableException.EXTRA_STRING_CLASS_NAME, exceptionClass)
|
||||
putSerializable(ReportableException.EXTRA_STRING_TRACE, exceptionTrace)
|
||||
putString(ReportableException.EXTRA_STRING_APP_VERSION, appVersion)
|
||||
putBoolean(ReportableException.EXTRA_BOOLEAN_IS_UNCAUGHT, isUncaught)
|
||||
putLong(ReportableException.EXTRA_LONG_WALLTIME_MILLIS, time.toEpochMilliseconds())
|
||||
}
|
||||
internal fun ReportableException.toBundle() =
|
||||
Bundle().apply {
|
||||
// Although Exception is Serializable, some Kotlin Coroutines exception classes break this
|
||||
// API contract. Therefore we have to convert to a string here.
|
||||
putSerializable(ReportableException.EXTRA_STRING_CLASS_NAME, exceptionClass)
|
||||
putSerializable(ReportableException.EXTRA_STRING_TRACE, exceptionTrace)
|
||||
putString(ReportableException.EXTRA_STRING_APP_VERSION, appVersion)
|
||||
putBoolean(ReportableException.EXTRA_BOOLEAN_IS_UNCAUGHT, isUncaught)
|
||||
putLong(ReportableException.EXTRA_LONG_WALLTIME_MILLIS, time.toEpochMilliseconds())
|
||||
}
|
||||
|
||||
internal fun ReportableException.Companion.fromBundle(bundle: Bundle): ReportableException {
|
||||
val className = bundle.getString(EXTRA_STRING_CLASS_NAME)!!
|
||||
|
|
|
@ -12,14 +12,17 @@ internal class AndroidUncaughtExceptionHandler(
|
|||
context: Context,
|
||||
private val defaultUncaughtExceptionHandler: Thread.UncaughtExceptionHandler
|
||||
) : Thread.UncaughtExceptionHandler {
|
||||
|
||||
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 isUseSecondaryProcess = applicationContext.resources
|
||||
.getBoolean(R.bool.co_electriccoin_zcash_crash_is_use_secondary_process)
|
||||
val isUseSecondaryProcess =
|
||||
applicationContext.resources
|
||||
.getBoolean(R.bool.co_electriccoin_zcash_crash_is_use_secondary_process)
|
||||
|
||||
if (isUseSecondaryProcess) {
|
||||
applicationContext.sendBroadcast(ExceptionReceiver.newIntent(applicationContext, reportableException))
|
||||
|
@ -31,7 +34,6 @@ internal class AndroidUncaughtExceptionHandler(
|
|||
}
|
||||
|
||||
companion object {
|
||||
|
||||
private val isInitialized = AtomicBoolean(false)
|
||||
|
||||
/**
|
||||
|
|
|
@ -8,16 +8,18 @@ import kotlinx.coroutines.GlobalScope
|
|||
|
||||
@OptIn(kotlinx.coroutines.DelicateCoroutinesApi::class)
|
||||
class ExceptionReceiver : CoroutineBroadcastReceiver(GlobalScope) {
|
||||
|
||||
override suspend fun onReceiveSuspend(context: Context, intent: Intent) {
|
||||
val reportableException = intent.extras?.let { ReportableException.fromBundle(it) }
|
||||
?: return
|
||||
override suspend fun onReceiveSuspend(
|
||||
context: Context,
|
||||
intent: Intent
|
||||
) {
|
||||
val reportableException =
|
||||
intent.extras?.let { ReportableException.fromBundle(it) }
|
||||
?: return
|
||||
|
||||
AndroidExceptionReporter.reportException(context, reportableException)
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
/**
|
||||
* @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.
|
||||
*/
|
||||
internal class LocalCrashReporter(private val applicationContext: Context) : CrashReporter {
|
||||
|
||||
private val crashReportingScope = CoroutineScope(SupervisorJob() + Dispatchers.Main)
|
||||
|
||||
@AnyThread
|
||||
|
@ -36,10 +35,11 @@ internal class LocalCrashReporter(private val applicationContext: Context) : Cra
|
|||
}
|
||||
|
||||
companion object {
|
||||
private val lazyWithArgument = LazyWithArgument<Context, CrashReporter> {
|
||||
AndroidUncaughtExceptionHandler.register(it)
|
||||
LocalCrashReporter(it.applicationContext)
|
||||
}
|
||||
private val lazyWithArgument =
|
||||
LazyWithArgument<Context, CrashReporter> {
|
||||
AndroidUncaughtExceptionHandler.register(it)
|
||||
LocalCrashReporter(it.applicationContext)
|
||||
}
|
||||
|
||||
fun getInstance(context: Context): CrashReporter {
|
||||
return lazyWithArgument.getInstance(context)
|
||||
|
|
|
@ -9,6 +9,5 @@ data class ReportableException(
|
|||
val isUncaught: Boolean,
|
||||
val time: Instant
|
||||
) {
|
||||
|
||||
companion object
|
||||
}
|
||||
|
|
|
@ -14,8 +14,10 @@ object ExceptionPath {
|
|||
const val TYPE = "txt"
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
fun newExceptionFileName(exception: ReportableException, uuid: UUID = UUID.randomUUID()) =
|
||||
"${exception.time.epochSeconds}$SEPARATOR$uuid$SEPARATOR${exception.exceptionClass}$SEPARATOR${exception.isUncaught}.$TYPE"
|
||||
fun newExceptionFileName(
|
||||
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
|
||||
@Suppress("ThrowsCount")
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
@file:Suppress("ktlint:filename")
|
||||
@file:Suppress("ktlint:standard:filename")
|
||||
|
||||
package co.electriccoin.zcash.crash
|
||||
|
||||
|
@ -8,12 +8,13 @@ import kotlinx.coroutines.withContext
|
|||
import java.io.File
|
||||
|
||||
suspend fun ReportableException.write(path: File) {
|
||||
val exceptionString = buildString {
|
||||
appendLine("App version: $appVersion")
|
||||
appendLine("Is uncaught: $isUncaught")
|
||||
appendLine("Time: $time")
|
||||
append(exceptionTrace)
|
||||
}
|
||||
val exceptionString =
|
||||
buildString {
|
||||
appendLine("App version: $appVersion")
|
||||
appendLine("Is uncaught: $isUncaught")
|
||||
appendLine("Time: $time")
|
||||
append(exceptionTrace)
|
||||
}
|
||||
|
||||
withContext(Dispatchers.IO) {
|
||||
path.writeAtomically { tempFile ->
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
@file:Suppress("ktlint:filename")
|
||||
@file:Suppress("ktlint:standard:filename")
|
||||
|
||||
package co.electriccoin.zcash.crash
|
||||
|
||||
|
|
|
@ -143,7 +143,7 @@ FULLADLE_VERSION=0.17.4
|
|||
GOOGLE_PLAY_SERVICES_GRADLE_PLUGIN_VERSION=4.3.15
|
||||
GRADLE_VERSIONS_PLUGIN_VERSION=0.47.0
|
||||
JGIT_VERSION=6.4.0.202211300538-r
|
||||
KTLINT_VERSION=0.49.0
|
||||
KTLINT_VERSION=1.0.1
|
||||
|
||||
ACCOMPANIST_PERMISSIONS_VERSION=0.32.0
|
||||
ANDROIDX_ACTIVITY_VERSION=1.8.1
|
||||
|
|
|
@ -4,10 +4,12 @@ import co.electriccoin.zcash.preference.model.entry.PreferenceKey
|
|||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
interface PreferenceProvider {
|
||||
|
||||
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?
|
||||
|
||||
|
|
|
@ -6,19 +6,22 @@ data class BooleanPreferenceDefault(
|
|||
override val key: PreferenceKey,
|
||||
private val defaultValue: Boolean
|
||||
) : PreferenceDefault<Boolean> {
|
||||
|
||||
@Suppress("SwallowedException")
|
||||
override suspend fun getValue(preferenceProvider: PreferenceProvider) = preferenceProvider.getString(key)?.let {
|
||||
try {
|
||||
it.toBooleanStrict()
|
||||
} catch (e: IllegalArgumentException) {
|
||||
// TODO [#32]: Log coercion failure instead of just silently returning default
|
||||
// TODO [#32]: https://github.com/Electric-Coin-Company/zashi-android/issues/32
|
||||
defaultValue
|
||||
}
|
||||
} ?: defaultValue
|
||||
override suspend fun getValue(preferenceProvider: PreferenceProvider) =
|
||||
preferenceProvider.getString(key)?.let {
|
||||
try {
|
||||
it.toBooleanStrict()
|
||||
} catch (e: IllegalArgumentException) {
|
||||
// TODO [#32]: Log coercion failure instead of just silently returning default
|
||||
// TODO [#32]: https://github.com/Electric-Coin-Company/zashi-android/issues/32
|
||||
defaultValue
|
||||
}
|
||||
} ?: defaultValue
|
||||
|
||||
override suspend fun putValue(preferenceProvider: PreferenceProvider, newValue: Boolean) {
|
||||
override suspend fun putValue(
|
||||
preferenceProvider: PreferenceProvider,
|
||||
newValue: Boolean
|
||||
) {
|
||||
preferenceProvider.putString(key, newValue.toString())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,18 +6,21 @@ data class IntegerPreferenceDefault(
|
|||
override val key: PreferenceKey,
|
||||
private val defaultValue: Int
|
||||
) : PreferenceDefault<Int> {
|
||||
override suspend fun getValue(preferenceProvider: PreferenceProvider) =
|
||||
preferenceProvider.getString(key)?.let {
|
||||
try {
|
||||
it.toInt()
|
||||
} catch (e: NumberFormatException) {
|
||||
// TODO [#32]: Log coercion failure instead of just silently returning default
|
||||
// TODO [#32]: https://github.com/Electric-Coin-Company/zashi-android/issues/32
|
||||
defaultValue
|
||||
}
|
||||
} ?: defaultValue
|
||||
|
||||
override suspend fun getValue(preferenceProvider: PreferenceProvider) = preferenceProvider.getString(key)?.let {
|
||||
try {
|
||||
it.toInt()
|
||||
} catch (e: NumberFormatException) {
|
||||
// TODO [#32]: Log coercion failure instead of just silently returning default
|
||||
// TODO [#32]: https://github.com/Electric-Coin-Company/zashi-android/issues/32
|
||||
defaultValue
|
||||
}
|
||||
} ?: defaultValue
|
||||
|
||||
override suspend fun putValue(preferenceProvider: PreferenceProvider, newValue: Int) {
|
||||
override suspend fun putValue(
|
||||
preferenceProvider: PreferenceProvider,
|
||||
newValue: Int
|
||||
) {
|
||||
preferenceProvider.putString(key, newValue.toString())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,16 +11,16 @@ import kotlinx.coroutines.flow.map
|
|||
* variation in default value. Clients define the key and default value together, rather than just
|
||||
* the key.
|
||||
*/
|
||||
/*
|
||||
* API note: the default value is not available through the public interface in order to prevent
|
||||
* clients from accidentally using the default value instead of the preference value.
|
||||
*
|
||||
* Implementation note: although primitives would be nice, Objects don't increase memory usage much.
|
||||
* The autoboxing cache solves Booleans, and Strings are already objects, so that just leaves Integers.
|
||||
* Overall the number of Integer preference entries is expected to be low compared to Booleans,
|
||||
* and perhaps many Integer values will also fit within the autoboxing cache.
|
||||
*/
|
||||
interface PreferenceDefault<T> {
|
||||
/*
|
||||
* 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.
|
||||
*
|
||||
* Implementation note: although primitives would be nice, Objects don't increase memory usage much.
|
||||
* The autoboxing cache solves Booleans, and Strings are already objects, so that just leaves Integers.
|
||||
* Overall the number of Integer preference entries is expected to be low compared to Booleans,
|
||||
* and perhaps many Integer values will also fit within the autoboxing cache.
|
||||
*/
|
||||
|
||||
val key: PreferenceKey
|
||||
|
||||
|
@ -34,14 +34,18 @@ interface PreferenceDefault<T> {
|
|||
* @param preferenceProvider Provides actual preference values.
|
||||
* @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.
|
||||
* @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.
|
||||
*/
|
||||
fun observe(preferenceProvider: PreferenceProvider): Flow<T> = preferenceProvider.observe(key)
|
||||
.map { getValue(preferenceProvider) }
|
||||
.distinctUntilChanged()
|
||||
fun observe(preferenceProvider: PreferenceProvider): Flow<T> =
|
||||
preferenceProvider.observe(key)
|
||||
.map { getValue(preferenceProvider) }
|
||||
.distinctUntilChanged()
|
||||
}
|
||||
|
|
|
@ -6,11 +6,14 @@ data class StringPreferenceDefault(
|
|||
override val key: PreferenceKey,
|
||||
private val defaultValue: String
|
||||
) : PreferenceDefault<String> {
|
||||
override suspend fun getValue(preferenceProvider: PreferenceProvider) =
|
||||
preferenceProvider.getString(key)
|
||||
?: defaultValue
|
||||
|
||||
override suspend fun getValue(preferenceProvider: PreferenceProvider) = preferenceProvider.getString(key)
|
||||
?: defaultValue
|
||||
|
||||
override suspend fun putValue(preferenceProvider: PreferenceProvider, newValue: String) {
|
||||
override suspend fun putValue(
|
||||
preferenceProvider: PreferenceProvider,
|
||||
newValue: String
|
||||
) {
|
||||
preferenceProvider.putString(key, newValue)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,32 +16,38 @@ class BooleanPreferenceDefaultTest {
|
|||
}
|
||||
|
||||
@Test
|
||||
fun value_default_true() = runTest {
|
||||
val entry = BooleanPreferenceDefaultFixture.newTrue()
|
||||
assertTrue(entry.getValue(MockPreferenceProvider()))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun value_default_false() = runTest {
|
||||
val entry = BooleanPreferenceDefaultFixture.newFalse()
|
||||
assertFalse(entry.getValue(MockPreferenceProvider()))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun value_from_config_false() = runTest {
|
||||
val entry = BooleanPreferenceDefaultFixture.newTrue()
|
||||
val mockPreferenceProvider = MockPreferenceProvider {
|
||||
mutableMapOf(BooleanPreferenceDefaultFixture.KEY.key to false.toString())
|
||||
fun value_default_true() =
|
||||
runTest {
|
||||
val entry = BooleanPreferenceDefaultFixture.newTrue()
|
||||
assertTrue(entry.getValue(MockPreferenceProvider()))
|
||||
}
|
||||
assertFalse(entry.getValue(mockPreferenceProvider))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun value_from_config_true() = runTest {
|
||||
val entry = BooleanPreferenceDefaultFixture.newTrue()
|
||||
val mockPreferenceProvider = MockPreferenceProvider {
|
||||
mutableMapOf(BooleanPreferenceDefaultFixture.KEY.key to true.toString())
|
||||
fun value_default_false() =
|
||||
runTest {
|
||||
val entry = BooleanPreferenceDefaultFixture.newFalse()
|
||||
assertFalse(entry.getValue(MockPreferenceProvider()))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun value_from_config_false() =
|
||||
runTest {
|
||||
val entry = BooleanPreferenceDefaultFixture.newTrue()
|
||||
val mockPreferenceProvider =
|
||||
MockPreferenceProvider {
|
||||
mutableMapOf(BooleanPreferenceDefaultFixture.KEY.key to false.toString())
|
||||
}
|
||||
assertFalse(entry.getValue(mockPreferenceProvider))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun value_from_config_true() =
|
||||
runTest {
|
||||
val entry = BooleanPreferenceDefaultFixture.newTrue()
|
||||
val mockPreferenceProvider =
|
||||
MockPreferenceProvider {
|
||||
mutableMapOf(BooleanPreferenceDefaultFixture.KEY.key to true.toString())
|
||||
}
|
||||
assertTrue(entry.getValue(mockPreferenceProvider))
|
||||
}
|
||||
assertTrue(entry.getValue(mockPreferenceProvider))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,20 +15,23 @@ class IntegerPreferenceDefaultTest {
|
|||
}
|
||||
|
||||
@Test
|
||||
fun value_default() = runTest {
|
||||
val entry = IntegerPreferenceDefaultFixture.new()
|
||||
assertEquals(IntegerPreferenceDefaultFixture.DEFAULT_VALUE, entry.getValue(MockPreferenceProvider()))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun value_override() = runTest {
|
||||
val expected = IntegerPreferenceDefaultFixture.DEFAULT_VALUE + 5
|
||||
|
||||
val entry = IntegerPreferenceDefaultFixture.new()
|
||||
val mockPreferenceProvider = MockPreferenceProvider {
|
||||
mutableMapOf(StringDefaultPreferenceFixture.KEY.key to expected.toString())
|
||||
fun value_default() =
|
||||
runTest {
|
||||
val entry = IntegerPreferenceDefaultFixture.new()
|
||||
assertEquals(IntegerPreferenceDefaultFixture.DEFAULT_VALUE, entry.getValue(MockPreferenceProvider()))
|
||||
}
|
||||
|
||||
assertEquals(expected, entry.getValue(mockPreferenceProvider))
|
||||
}
|
||||
@Test
|
||||
fun value_override() =
|
||||
runTest {
|
||||
val expected = IntegerPreferenceDefaultFixture.DEFAULT_VALUE + 5
|
||||
|
||||
val entry = IntegerPreferenceDefaultFixture.new()
|
||||
val mockPreferenceProvider =
|
||||
MockPreferenceProvider {
|
||||
mutableMapOf(StringDefaultPreferenceFixture.KEY.key to expected.toString())
|
||||
}
|
||||
|
||||
assertEquals(expected, entry.getValue(mockPreferenceProvider))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,19 +14,22 @@ class StringPreferenceDefaultTest {
|
|||
}
|
||||
|
||||
@Test
|
||||
fun value_default() = runTest {
|
||||
val entry = StringDefaultPreferenceFixture.new()
|
||||
assertEquals(StringDefaultPreferenceFixture.DEFAULT_VALUE, entry.getValue(MockPreferenceProvider()))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun value_override() = runTest {
|
||||
val entry = StringDefaultPreferenceFixture.new()
|
||||
|
||||
val mockPreferenceProvider = MockPreferenceProvider {
|
||||
mutableMapOf(StringDefaultPreferenceFixture.KEY.key to "override")
|
||||
fun value_default() =
|
||||
runTest {
|
||||
val entry = StringDefaultPreferenceFixture.new()
|
||||
assertEquals(StringDefaultPreferenceFixture.DEFAULT_VALUE, entry.getValue(MockPreferenceProvider()))
|
||||
}
|
||||
|
||||
assertEquals("override", entry.getValue(mockPreferenceProvider))
|
||||
}
|
||||
@Test
|
||||
fun value_override() =
|
||||
runTest {
|
||||
val entry = StringDefaultPreferenceFixture.new()
|
||||
|
||||
val mockPreferenceProvider =
|
||||
MockPreferenceProvider {
|
||||
mutableMapOf(StringDefaultPreferenceFixture.KEY.key to "override")
|
||||
}
|
||||
|
||||
assertEquals("override", entry.getValue(mockPreferenceProvider))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,7 +11,6 @@ import kotlinx.coroutines.flow.flow
|
|||
class MockPreferenceProvider(
|
||||
mutableMapFactory: () -> MutableMap<String, String?> = { mutableMapOf() }
|
||||
) : PreferenceProvider {
|
||||
|
||||
private val map = mutableMapFactory()
|
||||
|
||||
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 putString(key: PreferenceKey, value: String?) {
|
||||
override suspend fun putString(
|
||||
key: PreferenceKey,
|
||||
value: String?
|
||||
) {
|
||||
map[key.key] = value
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,6 +5,8 @@ import co.electriccoin.zcash.preference.model.entry.PreferenceKey
|
|||
|
||||
object BooleanPreferenceDefaultFixture {
|
||||
val KEY = PreferenceKey("some_boolean_key") // $NON-NLS
|
||||
|
||||
fun newTrue() = BooleanPreferenceDefault(KEY, true)
|
||||
|
||||
fun newFalse() = BooleanPreferenceDefault(KEY, false)
|
||||
}
|
||||
|
|
|
@ -6,6 +6,9 @@ import co.electriccoin.zcash.preference.model.entry.PreferenceKey
|
|||
object IntegerPreferenceDefaultFixture {
|
||||
val KEY = PreferenceKey("some_string_key") // $NON-NLS
|
||||
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 {
|
||||
val KEY = PreferenceKey("some_string_key") // $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,61 +35,70 @@ class EncryptedPreferenceProviderTest {
|
|||
|
||||
@Test
|
||||
@SmallTest
|
||||
fun put_and_get_string() = runBlocking {
|
||||
val expectedValue = StringDefaultPreferenceFixture.DEFAULT_VALUE + "extra"
|
||||
fun put_and_get_string() =
|
||||
runBlocking {
|
||||
val expectedValue = StringDefaultPreferenceFixture.DEFAULT_VALUE + "extra"
|
||||
|
||||
val preferenceProvider = new().apply {
|
||||
putString(StringDefaultPreferenceFixture.KEY, expectedValue)
|
||||
val preferenceProvider =
|
||||
new().apply {
|
||||
putString(StringDefaultPreferenceFixture.KEY, expectedValue)
|
||||
}
|
||||
|
||||
assertEquals(expectedValue, StringDefaultPreferenceFixture.new().getValue(preferenceProvider))
|
||||
}
|
||||
|
||||
assertEquals(expectedValue, StringDefaultPreferenceFixture.new().getValue(preferenceProvider))
|
||||
}
|
||||
|
||||
@Test
|
||||
@SmallTest
|
||||
fun hasKey_false() = runBlocking {
|
||||
val preferenceProvider = new()
|
||||
fun hasKey_false() =
|
||||
runBlocking {
|
||||
val preferenceProvider = new()
|
||||
|
||||
assertFalse(preferenceProvider.hasKey(StringDefaultPreferenceFixture.new().key))
|
||||
}
|
||||
assertFalse(preferenceProvider.hasKey(StringDefaultPreferenceFixture.new().key))
|
||||
}
|
||||
|
||||
@Test
|
||||
@SmallTest
|
||||
fun put_and_check_key() = runBlocking {
|
||||
val expectedValue = StringDefaultPreferenceFixture.DEFAULT_VALUE + "extra"
|
||||
fun put_and_check_key() =
|
||||
runBlocking {
|
||||
val expectedValue = StringDefaultPreferenceFixture.DEFAULT_VALUE + "extra"
|
||||
|
||||
val preferenceProvider = new().apply {
|
||||
putString(StringDefaultPreferenceFixture.KEY, expectedValue)
|
||||
val preferenceProvider =
|
||||
new().apply {
|
||||
putString(StringDefaultPreferenceFixture.KEY, expectedValue)
|
||||
}
|
||||
|
||||
assertTrue(preferenceProvider.hasKey(StringDefaultPreferenceFixture.new().key))
|
||||
}
|
||||
|
||||
assertTrue(preferenceProvider.hasKey(StringDefaultPreferenceFixture.new().key))
|
||||
}
|
||||
|
||||
// Note: this test case relies on undocumented implementation details of SharedPreferences
|
||||
// e.g. the directory path and the fact the preferences are stored as XML
|
||||
@Test
|
||||
@SmallTest
|
||||
fun verify_no_plaintext() = runBlocking {
|
||||
val expectedValue = StringDefaultPreferenceFixture.DEFAULT_VALUE + "extra"
|
||||
fun verify_no_plaintext() =
|
||||
runBlocking {
|
||||
val expectedValue = StringDefaultPreferenceFixture.DEFAULT_VALUE + "extra"
|
||||
|
||||
new().apply {
|
||||
putString(StringDefaultPreferenceFixture.KEY, expectedValue)
|
||||
new().apply {
|
||||
putString(StringDefaultPreferenceFixture.KEY, expectedValue)
|
||||
}
|
||||
|
||||
val text =
|
||||
File(
|
||||
File(ApplicationProvider.getApplicationContext<Context>().dataDir, "shared_prefs"),
|
||||
"$FILENAME.xml"
|
||||
).readText()
|
||||
|
||||
assertFalse(text.contains(expectedValue))
|
||||
assertFalse(text.contains(StringDefaultPreferenceFixture.KEY.key))
|
||||
}
|
||||
|
||||
val text = File(
|
||||
File(ApplicationProvider.getApplicationContext<Context>().dataDir, "shared_prefs"),
|
||||
"$FILENAME.xml"
|
||||
).readText()
|
||||
|
||||
assertFalse(text.contains(expectedValue))
|
||||
assertFalse(text.contains(StringDefaultPreferenceFixture.KEY.key))
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val FILENAME = "encrypted_preference_test"
|
||||
private suspend fun new() = AndroidPreferenceProvider.newEncrypted(
|
||||
ApplicationProvider.getApplicationContext(),
|
||||
FILENAME
|
||||
)
|
||||
|
||||
private suspend fun new() =
|
||||
AndroidPreferenceProvider.newEncrypted(
|
||||
ApplicationProvider.getApplicationContext(),
|
||||
FILENAME
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -33,41 +33,48 @@ class StandardPreferenceProviderTest {
|
|||
|
||||
@Test
|
||||
@SmallTest
|
||||
fun put_and_get_string() = runBlocking {
|
||||
val expectedValue = StringDefaultPreferenceFixture.DEFAULT_VALUE + "extra"
|
||||
fun put_and_get_string() =
|
||||
runBlocking {
|
||||
val expectedValue = StringDefaultPreferenceFixture.DEFAULT_VALUE + "extra"
|
||||
|
||||
val preferenceProvider = new().apply {
|
||||
putString(StringDefaultPreferenceFixture.KEY, expectedValue)
|
||||
val preferenceProvider =
|
||||
new().apply {
|
||||
putString(StringDefaultPreferenceFixture.KEY, expectedValue)
|
||||
}
|
||||
|
||||
assertEquals(expectedValue, StringDefaultPreferenceFixture.new().getValue(preferenceProvider))
|
||||
}
|
||||
|
||||
assertEquals(expectedValue, StringDefaultPreferenceFixture.new().getValue(preferenceProvider))
|
||||
}
|
||||
|
||||
@Test
|
||||
@SmallTest
|
||||
fun hasKey_false() = runBlocking {
|
||||
val preferenceProvider = new()
|
||||
fun hasKey_false() =
|
||||
runBlocking {
|
||||
val preferenceProvider = new()
|
||||
|
||||
assertFalse(preferenceProvider.hasKey(StringDefaultPreferenceFixture.new().key))
|
||||
}
|
||||
assertFalse(preferenceProvider.hasKey(StringDefaultPreferenceFixture.new().key))
|
||||
}
|
||||
|
||||
@Test
|
||||
@SmallTest
|
||||
fun put_and_check_key() = runBlocking {
|
||||
val expectedValue = StringDefaultPreferenceFixture.DEFAULT_VALUE + "extra"
|
||||
fun put_and_check_key() =
|
||||
runBlocking {
|
||||
val expectedValue = StringDefaultPreferenceFixture.DEFAULT_VALUE + "extra"
|
||||
|
||||
val preferenceProvider = new().apply {
|
||||
putString(StringDefaultPreferenceFixture.KEY, expectedValue)
|
||||
val preferenceProvider =
|
||||
new().apply {
|
||||
putString(StringDefaultPreferenceFixture.KEY, expectedValue)
|
||||
}
|
||||
|
||||
assertTrue(preferenceProvider.hasKey(StringDefaultPreferenceFixture.new().key))
|
||||
}
|
||||
|
||||
assertTrue(preferenceProvider.hasKey(StringDefaultPreferenceFixture.new().key))
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val FILENAME = "encrypted_preference_test"
|
||||
private suspend fun new() = AndroidPreferenceProvider.newStandard(
|
||||
ApplicationProvider.getApplicationContext(),
|
||||
FILENAME
|
||||
)
|
||||
|
||||
private suspend fun new() =
|
||||
AndroidPreferenceProvider.newStandard(
|
||||
ApplicationProvider.getApplicationContext(),
|
||||
FILENAME
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,6 +6,7 @@ import co.electriccoin.zcash.preference.model.entry.StringPreferenceDefault
|
|||
object StringDefaultPreferenceFixture {
|
||||
val KEY = PreferenceKey("some_string_key") // $NON-NLS
|
||||
const val DEFAULT_VALUE = "some_default_value" // $NON-NLS
|
||||
|
||||
fun new(
|
||||
preferenceKey: PreferenceKey = KEY,
|
||||
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
|
||||
* 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(
|
||||
private val sharedPreferences: SharedPreferences,
|
||||
private val dispatcher: CoroutineDispatcher
|
||||
) : 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) {
|
||||
sharedPreferences.contains(key.key)
|
||||
}
|
||||
override suspend fun hasKey(key: PreferenceKey) =
|
||||
withContext(dispatcher) {
|
||||
sharedPreferences.contains(key.key)
|
||||
}
|
||||
|
||||
@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()
|
||||
|
||||
editor.putString(key.key, value)
|
||||
|
@ -50,65 +54,77 @@ class AndroidPreferenceProvider(
|
|||
Unit
|
||||
}
|
||||
|
||||
override suspend fun getString(key: PreferenceKey) = withContext(dispatcher) {
|
||||
sharedPreferences.getString(key.key, null)
|
||||
}
|
||||
override suspend fun getString(key: PreferenceKey) =
|
||||
withContext(dispatcher) {
|
||||
sharedPreferences.getString(key.key, null)
|
||||
}
|
||||
|
||||
override fun observe(key: PreferenceKey): Flow<String?> = callbackFlow<Unit> {
|
||||
val listener = SharedPreferences.OnSharedPreferenceChangeListener { _, _ ->
|
||||
// Callback on main thread
|
||||
override fun observe(key: PreferenceKey): Flow<String?> =
|
||||
callbackFlow<Unit> {
|
||||
val listener =
|
||||
SharedPreferences.OnSharedPreferenceChangeListener { _, _ ->
|
||||
// Callback on main thread
|
||||
trySend(Unit)
|
||||
}
|
||||
sharedPreferences.registerOnSharedPreferenceChangeListener(listener)
|
||||
|
||||
// Kickstart the emissions
|
||||
trySend(Unit)
|
||||
}
|
||||
sharedPreferences.registerOnSharedPreferenceChangeListener(listener)
|
||||
|
||||
// Kickstart the emissions
|
||||
trySend(Unit)
|
||||
|
||||
awaitClose {
|
||||
sharedPreferences.unregisterOnSharedPreferenceChangeListener(listener)
|
||||
}
|
||||
}.flowOn(dispatcher)
|
||||
.map { getString(key) }
|
||||
awaitClose {
|
||||
sharedPreferences.unregisterOnSharedPreferenceChangeListener(listener)
|
||||
}
|
||||
}.flowOn(dispatcher)
|
||||
.map { getString(key) }
|
||||
|
||||
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 we don't clean up the thread afterwards.
|
||||
*/
|
||||
val singleThreadedDispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher()
|
||||
|
||||
val sharedPreferences = withContext(singleThreadedDispatcher) {
|
||||
context.getSharedPreferences(filename, Context.MODE_PRIVATE)
|
||||
}
|
||||
val sharedPreferences =
|
||||
withContext(singleThreadedDispatcher) {
|
||||
context.getSharedPreferences(filename, Context.MODE_PRIVATE)
|
||||
}
|
||||
|
||||
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 we don't clean up the thread afterwards.
|
||||
*/
|
||||
val singleThreadedDispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher()
|
||||
|
||||
val mainKey = withContext(singleThreadedDispatcher) {
|
||||
@Suppress("BlockingMethodInNonBlockingContext")
|
||||
MasterKey.Builder(context).apply {
|
||||
setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
|
||||
}.build()
|
||||
}
|
||||
val mainKey =
|
||||
withContext(singleThreadedDispatcher) {
|
||||
@Suppress("BlockingMethodInNonBlockingContext")
|
||||
MasterKey.Builder(context).apply {
|
||||
setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
|
||||
}.build()
|
||||
}
|
||||
|
||||
val sharedPreferences = withContext(singleThreadedDispatcher) {
|
||||
@Suppress("BlockingMethodInNonBlockingContext")
|
||||
EncryptedSharedPreferences.create(
|
||||
context,
|
||||
filename,
|
||||
mainKey,
|
||||
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
|
||||
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
|
||||
)
|
||||
}
|
||||
val sharedPreferences =
|
||||
withContext(singleThreadedDispatcher) {
|
||||
@Suppress("BlockingMethodInNonBlockingContext")
|
||||
EncryptedSharedPreferences.create(
|
||||
context,
|
||||
filename,
|
||||
mainKey,
|
||||
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
|
||||
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
|
||||
)
|
||||
}
|
||||
|
||||
return AndroidPreferenceProvider(sharedPreferences, singleThreadedDispatcher)
|
||||
}
|
||||
|
|
|
@ -8,52 +8,57 @@ import org.junit.Test
|
|||
import kotlin.test.assertEquals
|
||||
|
||||
class PercentDecimalExtTest {
|
||||
@Test
|
||||
@SmallTest
|
||||
fun parse_non_zero_percent_decimal_test() =
|
||||
runTest {
|
||||
val parsed = PercentDecimal(0.1234f).toPercentageWithDecimal()
|
||||
|
||||
assertEquals("12${MonetarySeparators.current().decimal}34", parsed)
|
||||
}
|
||||
|
||||
@Test
|
||||
@SmallTest
|
||||
fun parse_non_zero_percent_decimal_test() = runTest {
|
||||
val parsed = PercentDecimal(0.1234f).toPercentageWithDecimal()
|
||||
fun parse_zero_percent_decimal_test() =
|
||||
runTest {
|
||||
val parsed = PercentDecimal(0.0000f).toPercentageWithDecimal()
|
||||
|
||||
assertEquals("12${MonetarySeparators.current().decimal}34", parsed)
|
||||
}
|
||||
assertEquals("0${MonetarySeparators.current().decimal}00", parsed)
|
||||
}
|
||||
|
||||
@Test
|
||||
@SmallTest
|
||||
fun parse_zero_percent_decimal_test() = runTest {
|
||||
val parsed = PercentDecimal(0.0000f).toPercentageWithDecimal()
|
||||
fun parse_max_percent_decimal_test() =
|
||||
runTest {
|
||||
val parsed = PercentDecimal(1f).toPercentageWithDecimal()
|
||||
|
||||
assertEquals("0${MonetarySeparators.current().decimal}00", parsed)
|
||||
}
|
||||
assertEquals("100${MonetarySeparators.current().decimal}00", parsed)
|
||||
}
|
||||
|
||||
@Test
|
||||
@SmallTest
|
||||
fun parse_max_percent_decimal_test() = runTest {
|
||||
val parsed = PercentDecimal(1f).toPercentageWithDecimal()
|
||||
fun parse_min_percent_decimal_test() =
|
||||
runTest {
|
||||
val parsed = PercentDecimal(0f).toPercentageWithDecimal()
|
||||
|
||||
assertEquals("100${MonetarySeparators.current().decimal}00", parsed)
|
||||
}
|
||||
assertEquals("0${MonetarySeparators.current().decimal}00", parsed)
|
||||
}
|
||||
|
||||
@Test
|
||||
@SmallTest
|
||||
fun parse_min_percent_decimal_test() = runTest {
|
||||
val parsed = PercentDecimal(0f).toPercentageWithDecimal()
|
||||
fun parse_round_down_percent_decimal_test() =
|
||||
runTest {
|
||||
val parsed = PercentDecimal(0.11111f).toPercentageWithDecimal()
|
||||
|
||||
assertEquals("0${MonetarySeparators.current().decimal}00", parsed)
|
||||
}
|
||||
assertEquals("11${MonetarySeparators.current().decimal}11", parsed)
|
||||
}
|
||||
|
||||
@Test
|
||||
@SmallTest
|
||||
fun parse_round_down_percent_decimal_test() = runTest {
|
||||
val parsed = PercentDecimal(0.11111f).toPercentageWithDecimal()
|
||||
fun parse_round_up_percent_decimal_test() =
|
||||
runTest {
|
||||
val parsed = PercentDecimal(0.11119f).toPercentageWithDecimal()
|
||||
|
||||
assertEquals("11${MonetarySeparators.current().decimal}11", parsed)
|
||||
}
|
||||
|
||||
@Test
|
||||
@SmallTest
|
||||
fun parse_round_up_percent_decimal_test() = runTest {
|
||||
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
|
||||
|
||||
class ZecRequestTest {
|
||||
|
||||
companion object {
|
||||
private const val URI: String = "zcash:tmXuTnE11JojToagTqxXUn6KvdxDE3iLKbp?amount=1&message=Hello%20world!"
|
||||
|
||||
|
@ -23,100 +22,111 @@ class ZecRequestTest {
|
|||
private val AMOUNT = Zatoshi(1)
|
||||
private val MESSAGE = ZecRequestMessage("Hello world!")
|
||||
private const val ADDRESS_STRING = "tmXuTnE11JojToagTqxXUn6KvdxDE3iLKbp"
|
||||
private val ADDRESS: WalletAddress.Unified = runBlocking {
|
||||
WalletAddress.Unified.new(ADDRESS_STRING)
|
||||
}
|
||||
private val ADDRESS: WalletAddress.Unified =
|
||||
runBlocking {
|
||||
WalletAddress.Unified.new(ADDRESS_STRING)
|
||||
}
|
||||
val REQUEST = ZecRequest(ADDRESS, AMOUNT, MESSAGE)
|
||||
}
|
||||
|
||||
@Test
|
||||
@SmallTest
|
||||
fun parse_uri_not_null() = runTest {
|
||||
val parsed = ZecRequest.fromUri(Zip321UriParseFixture.URI)
|
||||
fun parse_uri_not_null() =
|
||||
runTest {
|
||||
val parsed = ZecRequest.fromUri(Zip321UriParseFixture.URI)
|
||||
|
||||
assertNotNull(parsed)
|
||||
}
|
||||
assertNotNull(parsed)
|
||||
}
|
||||
|
||||
@Test
|
||||
@SmallTest
|
||||
fun parse_uri_valid_result() = runTest {
|
||||
val parsed = ZecRequest.fromUri(Zip321UriParseFixture.URI)
|
||||
fun parse_uri_valid_result() =
|
||||
runTest {
|
||||
val parsed = ZecRequest.fromUri(Zip321UriParseFixture.URI)
|
||||
|
||||
assertTrue(parsed.message.value.length <= ZecRequestMessage.MAX_MESSAGE_LENGTH)
|
||||
assertTrue(parsed.address.address.isNotEmpty())
|
||||
assertTrue(parsed.amount.value >= 0)
|
||||
}
|
||||
assertTrue(parsed.message.value.length <= ZecRequestMessage.MAX_MESSAGE_LENGTH)
|
||||
assertTrue(parsed.address.address.isNotEmpty())
|
||||
assertTrue(parsed.amount.value >= 0)
|
||||
}
|
||||
|
||||
@Test
|
||||
@SmallTest
|
||||
fun parse_uri_correct_result() = runTest {
|
||||
val parsed = ZecRequest.fromUri(Zip321UriParseFixture.URI)
|
||||
val expected = ZecRequest(
|
||||
WalletAddress.Unified.new(Zip321UriParseFixture.ADDRESS),
|
||||
Zip321UriParseFixture.AMOUNT,
|
||||
Zip321UriParseFixture.MESSAGE
|
||||
)
|
||||
fun parse_uri_correct_result() =
|
||||
runTest {
|
||||
val parsed = ZecRequest.fromUri(Zip321UriParseFixture.URI)
|
||||
val expected =
|
||||
ZecRequest(
|
||||
WalletAddress.Unified.new(Zip321UriParseFixture.ADDRESS),
|
||||
Zip321UriParseFixture.AMOUNT,
|
||||
Zip321UriParseFixture.MESSAGE
|
||||
)
|
||||
|
||||
assertEquals(parsed, expected)
|
||||
}
|
||||
assertEquals(parsed, expected)
|
||||
}
|
||||
|
||||
@Test
|
||||
@SmallTest
|
||||
// TODO [#397]: 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 {
|
||||
val parsed = ZecRequest.fromUri(URI)
|
||||
val expected = REQUEST
|
||||
val actual = ZecRequest(
|
||||
WalletAddress.Unified.new(Zip321UriParseFixture.ADDRESS),
|
||||
Zip321UriParseFixture.AMOUNT,
|
||||
Zip321UriParseFixture.MESSAGE
|
||||
)
|
||||
fun parse_uri_incorrect_result() =
|
||||
runTest {
|
||||
val parsed = ZecRequest.fromUri(URI)
|
||||
val expected = REQUEST
|
||||
val actual =
|
||||
ZecRequest(
|
||||
WalletAddress.Unified.new(Zip321UriParseFixture.ADDRESS),
|
||||
Zip321UriParseFixture.AMOUNT,
|
||||
Zip321UriParseFixture.MESSAGE
|
||||
)
|
||||
|
||||
assertNotEquals(parsed, expected)
|
||||
assertEquals(parsed, actual)
|
||||
}
|
||||
assertNotEquals(parsed, expected)
|
||||
assertEquals(parsed, actual)
|
||||
}
|
||||
|
||||
@Test
|
||||
@SmallTest
|
||||
fun build_uri_not_null() = runTest {
|
||||
val request = Zip321UriBuildFixture.REQUEST
|
||||
val built = request.toUri()
|
||||
fun build_uri_not_null() =
|
||||
runTest {
|
||||
val request = Zip321UriBuildFixture.REQUEST
|
||||
val built = request.toUri()
|
||||
|
||||
assertNotNull(built)
|
||||
}
|
||||
assertNotNull(built)
|
||||
}
|
||||
|
||||
@Test
|
||||
@SmallTest
|
||||
fun build_uri_valid_result() = runTest {
|
||||
val request = Zip321UriBuildFixture.REQUEST
|
||||
val built = request.toUri()
|
||||
fun build_uri_valid_result() =
|
||||
runTest {
|
||||
val request = Zip321UriBuildFixture.REQUEST
|
||||
val built = request.toUri()
|
||||
|
||||
assertTrue(built.isNotEmpty())
|
||||
assertTrue(built.startsWith("zcash"))
|
||||
}
|
||||
assertTrue(built.isNotEmpty())
|
||||
assertTrue(built.startsWith("zcash"))
|
||||
}
|
||||
|
||||
@Test
|
||||
@SmallTest
|
||||
fun built_uri_correct_result() = runTest {
|
||||
val request = Zip321UriBuildFixture.REQUEST
|
||||
val built = request.toUri()
|
||||
val expected = Zip321UriBuildFixture.URI
|
||||
fun built_uri_correct_result() =
|
||||
runTest {
|
||||
val request = Zip321UriBuildFixture.REQUEST
|
||||
val built = request.toUri()
|
||||
val expected = Zip321UriBuildFixture.URI
|
||||
|
||||
assertEquals(built, expected)
|
||||
}
|
||||
assertEquals(built, expected)
|
||||
}
|
||||
|
||||
@Test
|
||||
@SmallTest
|
||||
// TODO [#397]: 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 {
|
||||
val request = Zip321UriBuildFixture.REQUEST
|
||||
val built = request.toUri()
|
||||
val expected = URI
|
||||
val actual = Zip321UriBuildFixture.URI
|
||||
fun build_uri_incorrect_result() =
|
||||
runTest {
|
||||
val request = Zip321UriBuildFixture.REQUEST
|
||||
val built = request.toUri()
|
||||
val expected = URI
|
||||
val actual = Zip321UriBuildFixture.URI
|
||||
|
||||
assertNotEquals(built, expected)
|
||||
assertEquals(built, actual)
|
||||
}
|
||||
assertNotEquals(built, expected)
|
||||
assertEquals(built, actual)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,7 +7,6 @@ import org.junit.Test
|
|||
import kotlin.test.assertNotSame
|
||||
|
||||
class ZcashCurrencyTest {
|
||||
|
||||
@SmallTest
|
||||
@Test
|
||||
fun check_is_zec_type() {
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
@file:Suppress("ktlint:filename")
|
||||
@file:Suppress("ktlint:standard:filename")
|
||||
|
||||
package cash.z.ecc.sdk.extension
|
||||
|
||||
|
@ -12,9 +12,10 @@ fun PercentDecimal.toPercentageWithDecimal(decimalFormat: DecimalFormat = prepar
|
|||
return decimalFormat.format(decimal * 100)
|
||||
}
|
||||
|
||||
private fun preparePercentDecimalFormat(): DecimalFormat = DecimalFormat().apply {
|
||||
val monetarySeparators = MonetarySeparators.current()
|
||||
val localizedPattern = "##0${monetarySeparators.decimal}00"
|
||||
applyLocalizedPattern(localizedPattern)
|
||||
roundingMode = RoundingMode.HALF_UP
|
||||
}
|
||||
private fun preparePercentDecimalFormat(): DecimalFormat =
|
||||
DecimalFormat().apply {
|
||||
val monetarySeparators = MonetarySeparators.current()
|
||||
val localizedPattern = "##0${monetarySeparators.decimal}00"
|
||||
applyLocalizedPattern(localizedPattern)
|
||||
roundingMode = RoundingMode.HALF_UP
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
@file:Suppress("ktlint:filename")
|
||||
@file:Suppress("ktlint:standard:filename")
|
||||
|
||||
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.ZecSend
|
||||
|
||||
suspend fun Synchronizer.send(spendingKey: UnifiedSpendingKey, send: ZecSend) = sendToAddress(
|
||||
suspend fun Synchronizer.send(
|
||||
spendingKey: UnifiedSpendingKey,
|
||||
send: ZecSend
|
||||
) = sendToAddress(
|
||||
spendingKey,
|
||||
send.amount,
|
||||
send.destination.address,
|
||||
|
|
|
@ -9,7 +9,6 @@ import cash.z.ecc.android.sdk.model.ZcashNetwork
|
|||
import co.electriccoin.lightwallet.client.model.LightWalletEndpoint
|
||||
|
||||
object PersistableWalletFixture {
|
||||
|
||||
val NETWORK = ZcashNetwork.Mainnet
|
||||
|
||||
val ENDPOINT = LightWalletEndpoint.Mainnet
|
||||
|
|
|
@ -3,8 +3,9 @@ package cash.z.ecc.sdk.fixture
|
|||
import cash.z.ecc.android.sdk.model.SeedPhrase
|
||||
|
||||
object SeedPhraseFixture {
|
||||
@Suppress("MaxLineLength")
|
||||
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"
|
||||
const 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"
|
||||
|
||||
fun new(seedPhrase: String = SEED_PHRASE) = SeedPhrase.new(seedPhrase)
|
||||
}
|
||||
|
|
|
@ -9,15 +9,17 @@ import kotlinx.coroutines.runBlocking
|
|||
|
||||
object Zip321UriBuildFixture {
|
||||
// TODO [#161]: Pending SDK support
|
||||
const val URI: String = "zcash:Unified%20GitHub%20Issue%20#161?amount=123&message=Thank%20you%20" +
|
||||
"for%20your%20purchase"
|
||||
const val URI: String =
|
||||
"zcash:Unified%20GitHub%20Issue%20#161?amount=123&message=Thank%20you%20" +
|
||||
"for%20your%20purchase"
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
val AMOUNT = Zatoshi(123)
|
||||
val MESSAGE = ZecRequestMessage("Thank you for your purchase")
|
||||
val ADDRESS: WalletAddress.Unified = runBlocking {
|
||||
WalletAddress.Unified.new(WalletAddressFixture.UNIFIED_ADDRESS_STRING)
|
||||
}
|
||||
val ADDRESS: WalletAddress.Unified =
|
||||
runBlocking {
|
||||
WalletAddress.Unified.new(WalletAddressFixture.UNIFIED_ADDRESS_STRING)
|
||||
}
|
||||
val REQUEST = ZecRequest(ADDRESS, AMOUNT, MESSAGE)
|
||||
|
||||
// TODO [#397]: Waiting for an implementation of Uri parser in SDK project
|
||||
|
|
|
@ -8,8 +8,9 @@ import cash.z.ecc.sdk.model.ZecRequestMessage
|
|||
|
||||
object Zip321UriParseFixture {
|
||||
// TODO [#161]: Pending SDK support
|
||||
const val URI: String = "zcash:Unified%20GitHub%20Issue%20#161?amount=123&message=Thank%20you%20" +
|
||||
"for%20your%20purchase"
|
||||
const val URI: String =
|
||||
"zcash:Unified%20GitHub%20Issue%20#161?amount=123&message=Thank%20you%20" +
|
||||
"for%20your%20purchase"
|
||||
|
||||
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
|
||||
// Should return ZecRequest.fromUri(toParse) ideally, but it'd end up with an infinite loop for now.
|
||||
@Suppress("UNUSED_PARAMETER")
|
||||
suspend fun new(
|
||||
toParse: String = URI
|
||||
) = ZecRequest(WalletAddress.Unified.new(ADDRESS), AMOUNT, MESSAGE)
|
||||
suspend fun new(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
|
||||
sealed class SeedPhraseValidation {
|
||||
object BadCount : SeedPhraseValidation()
|
||||
|
||||
object BadWord : SeedPhraseValidation()
|
||||
|
||||
object FailedChecksum : SeedPhraseValidation()
|
||||
|
||||
class Valid(val seedPhrase: SeedPhrase) : SeedPhraseValidation()
|
||||
|
||||
companion object {
|
||||
|
|
|
@ -6,7 +6,6 @@ import cash.z.ecc.sdk.fixture.Zip321UriBuildFixture
|
|||
import cash.z.ecc.sdk.fixture.Zip321UriParseFixture
|
||||
|
||||
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]: https://github.com/Electric-Coin-Company/zcash-android-wallet-sdk/issues/397
|
||||
suspend fun toUri(): String {
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
@file:Suppress("ktlint:filename")
|
||||
@file:Suppress("ktlint:standard:filename")
|
||||
|
||||
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 AndroidManifest metadata for dynamic injection
|
||||
*/
|
||||
|
||||
/**
|
||||
* @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.
|
||||
|
|
|
@ -13,16 +13,19 @@ class AbstractProcessNameContentProviderTest {
|
|||
@SmallTest
|
||||
fun getProcessName_from_provider_info() {
|
||||
val expectedApplicationProcessName = "beep" // $NON-NLS
|
||||
val ctx: ContextWrapper = object : ContextWrapper(ApplicationProvider.getApplicationContext()) {
|
||||
override fun getApplicationInfo() = ApplicationInfo().apply {
|
||||
processName = expectedApplicationProcessName
|
||||
val ctx: ContextWrapper =
|
||||
object : ContextWrapper(ApplicationProvider.getApplicationContext()) {
|
||||
override fun getApplicationInfo() =
|
||||
ApplicationInfo().apply {
|
||||
processName = expectedApplicationProcessName
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val actualProcessName = AbstractProcessNameContentProvider.getProcessNameLegacy(
|
||||
ctx,
|
||||
ProviderInfo()
|
||||
)
|
||||
val actualProcessName =
|
||||
AbstractProcessNameContentProvider.getProcessNameLegacy(
|
||||
ctx,
|
||||
ProviderInfo()
|
||||
)
|
||||
|
||||
assertEquals(expectedApplicationProcessName, actualProcessName)
|
||||
}
|
||||
|
|
|
@ -13,13 +13,14 @@ class VersionCodeCompatTest {
|
|||
fun versionCodeCompat() {
|
||||
val expectedVersionCode = 123L
|
||||
|
||||
val packageInfo = PackageInfo().apply {
|
||||
@Suppress("Deprecation")
|
||||
versionCode = expectedVersionCode.toInt()
|
||||
if (AndroidApiVersion.isAtLeastT) {
|
||||
longVersionCode = expectedVersionCode
|
||||
val packageInfo =
|
||||
PackageInfo().apply {
|
||||
@Suppress("Deprecation")
|
||||
versionCode = expectedVersionCode.toInt()
|
||||
if (AndroidApiVersion.isAtLeastT) {
|
||||
longVersionCode = expectedVersionCode
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
assertEquals(expectedVersionCode, packageInfo.versionCodeCompat)
|
||||
}
|
||||
|
|
|
@ -11,7 +11,9 @@ object AndroidApiVersion {
|
|||
* [sdk].
|
||||
*/
|
||||
@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
|
||||
}
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
@file:Suppress("ktlint:filename")
|
||||
@file:Suppress("ktlint:standard:filename")
|
||||
|
||||
package co.electriccoin.zcash.spackle
|
||||
|
||||
|
@ -7,6 +7,7 @@ import android.content.ClipboardManager
|
|||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
suspend fun ClipboardManager.setPrimaryClipSuspend(data: ClipData) = withContext(Dispatchers.IO) {
|
||||
setPrimaryClip(data)
|
||||
}
|
||||
suspend fun ClipboardManager.setPrimaryClipSuspend(data: ClipData) =
|
||||
withContext(Dispatchers.IO) {
|
||||
setPrimaryClip(data)
|
||||
}
|
||||
|
|
|
@ -7,7 +7,6 @@ import android.widget.Toast
|
|||
import kotlinx.coroutines.runBlocking
|
||||
|
||||
object ClipboardManagerUtil {
|
||||
|
||||
fun copyToClipboard(
|
||||
context: Context,
|
||||
label: String,
|
||||
|
@ -15,10 +14,11 @@ object ClipboardManagerUtil {
|
|||
) {
|
||||
Twig.info { "Copied to clipboard: label: $label, value: $value" }
|
||||
val clipboardManager = context.getSystemService(ClipboardManager::class.java)
|
||||
val data = ClipData.newPlainText(
|
||||
label,
|
||||
value
|
||||
)
|
||||
val data =
|
||||
ClipData.newPlainText(
|
||||
label,
|
||||
value
|
||||
)
|
||||
if (AndroidApiVersion.isAtLeastT) {
|
||||
// API 33 and later implement their system Toast UI.
|
||||
clipboardManager.setPrimaryClip(data)
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
@file:Suppress("ktlint:filename")
|
||||
@file:Suppress("ktlint:standard:filename")
|
||||
|
||||
package co.electriccoin.zcash.spackle
|
||||
|
||||
|
@ -6,6 +6,7 @@ import android.content.Context
|
|||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
suspend fun Context.getExternalFilesDirSuspend(type: String?) = withContext(Dispatchers.IO) {
|
||||
getExternalFilesDir(type)
|
||||
}
|
||||
suspend fun Context.getExternalFilesDirSuspend(type: String?) =
|
||||
withContext(Dispatchers.IO) {
|
||||
getExternalFilesDir(type)
|
||||
}
|
||||
|
|
|
@ -11,7 +11,10 @@ import kotlinx.coroutines.launch
|
|||
* It is not recommended to cancel this scope.
|
||||
*/
|
||||
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()
|
||||
|
||||
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
|
||||
* not suitable for network calls.
|
||||
*/
|
||||
abstract suspend fun onReceiveSuspend(context: Context, intent: Intent)
|
||||
abstract suspend fun onReceiveSuspend(
|
||||
context: Context,
|
||||
intent: Intent
|
||||
)
|
||||
}
|
||||
|
|
|
@ -11,9 +11,10 @@ object EmulatorWtfUtil {
|
|||
private const val EMULATOR_WTF_SETTING = "emulator.wtf" // $NON-NLS
|
||||
private const val SETTING_TRUE = "true" // $NON-NLS
|
||||
|
||||
private val isEmulatorWtfCached = LazyWithArgument<Context, Boolean> {
|
||||
isEmulatorWtfImpl(it)
|
||||
}
|
||||
private val isEmulatorWtfCached =
|
||||
LazyWithArgument<Context, Boolean> {
|
||||
isEmulatorWtfImpl(it)
|
||||
}
|
||||
|
||||
/**
|
||||
* @return True if the environment is emulator.wtf
|
||||
|
|
|
@ -11,9 +11,10 @@ object FirebaseTestLabUtil {
|
|||
private const val FIREBASE_TEST_LAB_SETTING = "firebase.test.lab" // $NON-NLS
|
||||
private const val SETTING_TRUE = "true" // $NON-NLS
|
||||
|
||||
private val isFirebaseTestLabCached = LazyWithArgument<Context, Boolean> {
|
||||
isFirebaseTestLabImpl(it)
|
||||
}
|
||||
private val isFirebaseTestLabCached =
|
||||
LazyWithArgument<Context, Boolean> {
|
||||
isFirebaseTestLabImpl(it)
|
||||
}
|
||||
|
||||
/**
|
||||
* @return True if the environment is Firebase Test Lab.
|
||||
|
@ -24,10 +25,10 @@ object FirebaseTestLabUtil {
|
|||
/*
|
||||
* Per the documentation at https://firebase.google.com/docs/test-lab/android-studio
|
||||
*/
|
||||
// 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
|
||||
// since IPC may be involved.
|
||||
return runCatching {
|
||||
// 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
|
||||
// since IPC may be involved.
|
||||
SETTING_TRUE == Settings.System.getString(context.contentResolver, FIREBASE_TEST_LAB_SETTING)
|
||||
}.recover {
|
||||
// Fail-safe in case an error occurs
|
||||
|
|
|
@ -7,14 +7,20 @@ import android.os.Build
|
|||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
fun PackageManager.getPackageInfoCompat(packageName: String, flags: Long): PackageInfo =
|
||||
fun PackageManager.getPackageInfoCompat(
|
||||
packageName: String,
|
||||
flags: Long
|
||||
): PackageInfo =
|
||||
if (AndroidApiVersion.isAtLeastT) {
|
||||
getPackageInfoTPlus(packageName, flags)
|
||||
} else {
|
||||
getPackageInfoLegacy(packageName, flags)
|
||||
}
|
||||
|
||||
suspend fun PackageManager.getPackageInfoCompatSuspend(packageName: String, flags: Long): PackageInfo =
|
||||
suspend fun PackageManager.getPackageInfoCompatSuspend(
|
||||
packageName: String,
|
||||
flags: Long
|
||||
): PackageInfo =
|
||||
if (AndroidApiVersion.isAtLeastT) {
|
||||
withContext(Dispatchers.IO) { getPackageInfoTPlus(packageName, flags) }
|
||||
} else {
|
||||
|
@ -22,9 +28,13 @@ suspend fun PackageManager.getPackageInfoCompatSuspend(packageName: String, flag
|
|||
}
|
||||
|
||||
@TargetApi(Build.VERSION_CODES.TIRAMISU)
|
||||
private fun PackageManager.getPackageInfoTPlus(packageName: String, flags: Long) =
|
||||
getPackageInfo(packageName, PackageManager.PackageInfoFlags.of(flags))
|
||||
private fun PackageManager.getPackageInfoTPlus(
|
||||
packageName: String,
|
||||
flags: Long
|
||||
) = getPackageInfo(packageName, PackageManager.PackageInfoFlags.of(flags))
|
||||
|
||||
@Suppress("Deprecation")
|
||||
private fun PackageManager.getPackageInfoLegacy(packageName: String, flags: Long) =
|
||||
getPackageInfo(packageName, flags.toInt())
|
||||
private fun PackageManager.getPackageInfoLegacy(
|
||||
packageName: String,
|
||||
flags: Long
|
||||
) = getPackageInfo(packageName, flags.toInt())
|
||||
|
|
|
@ -4,7 +4,6 @@ import android.annotation.SuppressLint
|
|||
import android.os.StrictMode
|
||||
|
||||
object StrictModeCompat {
|
||||
|
||||
fun enableStrictMode(isCrashOnViolation: Boolean) {
|
||||
configureStrictMode(isCrashOnViolation)
|
||||
}
|
||||
|
|
|
@ -40,7 +40,10 @@ object Twig {
|
|||
|
||||
// JVMStatic is to simplify ProGuard/R8 rules for stripping this
|
||||
@JvmStatic
|
||||
fun verbose(throwable: Throwable, message: () -> String) {
|
||||
fun verbose(
|
||||
throwable: Throwable,
|
||||
message: () -> String
|
||||
) {
|
||||
Log.v(tag, formatMessage(message), throwable)
|
||||
}
|
||||
|
||||
|
@ -52,7 +55,10 @@ object Twig {
|
|||
|
||||
// JVMStatic is to simplify ProGuard/R8 rules for stripping this
|
||||
@JvmStatic
|
||||
fun debug(throwable: Throwable, message: () -> String) {
|
||||
fun debug(
|
||||
throwable: Throwable,
|
||||
message: () -> String
|
||||
) {
|
||||
Log.d(tag, formatMessage(message), throwable)
|
||||
}
|
||||
|
||||
|
@ -64,7 +70,10 @@ object Twig {
|
|||
|
||||
// JVMStatic is to simplify ProGuard/R8 rules for stripping this
|
||||
@JvmStatic
|
||||
fun info(throwable: Throwable, message: () -> String) {
|
||||
fun info(
|
||||
throwable: Throwable,
|
||||
message: () -> String
|
||||
) {
|
||||
Log.i(tag, formatMessage(message), throwable)
|
||||
}
|
||||
|
||||
|
@ -76,7 +85,10 @@ object Twig {
|
|||
|
||||
// JVMStatic is to simplify ProGuard/R8 rules for stripping this
|
||||
@JvmStatic
|
||||
fun warn(throwable: Throwable, message: () -> String) {
|
||||
fun warn(
|
||||
throwable: Throwable,
|
||||
message: () -> String
|
||||
) {
|
||||
Log.w(tag, formatMessage(message), throwable)
|
||||
}
|
||||
|
||||
|
@ -88,7 +100,10 @@ object Twig {
|
|||
|
||||
// JVMStatic is to simplify ProGuard/R8 rules for stripping this
|
||||
@JvmStatic
|
||||
fun error(throwable: Throwable, message: () -> String) {
|
||||
fun error(
|
||||
throwable: Throwable,
|
||||
message: () -> String
|
||||
) {
|
||||
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
|
||||
* properly processed to strip out logging messages.
|
||||
*/
|
||||
// JVMStatic is to simplify ProGuard/R8 rules for stripping this
|
||||
@JvmStatic
|
||||
@JvmStatic // JVMStatic is to simplify ProGuard/R8 rules for stripping this
|
||||
fun assertLoggingStripped() {
|
||||
@Suppress("MaxLineLength")
|
||||
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$
|
||||
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$
|
||||
}
|
||||
|
||||
private const val CALL_DEPTH = 4
|
||||
|
|
|
@ -5,11 +5,12 @@ import android.content.pm.PackageInfo
|
|||
import android.os.Build
|
||||
|
||||
val PackageInfo.versionCodeCompat
|
||||
get() = if (AndroidApiVersion.isAtLeastP) {
|
||||
getVersionCodePPlus()
|
||||
} else {
|
||||
versionCodeLegacy.toLong()
|
||||
}
|
||||
get() =
|
||||
if (AndroidApiVersion.isAtLeastP) {
|
||||
getVersionCodePPlus()
|
||||
} else {
|
||||
versionCodeLegacy.toLong()
|
||||
}
|
||||
|
||||
@Suppress("Deprecation")
|
||||
private val PackageInfo.versionCodeLegacy
|
||||
|
|
|
@ -20,16 +20,20 @@ import co.electriccoin.zcash.spackle.AndroidApiVersion
|
|||
open class AbstractProcessNameContentProvider : ContentProvider() {
|
||||
override fun onCreate() = true
|
||||
|
||||
override fun attachInfo(context: Context, info: ProviderInfo) {
|
||||
override fun attachInfo(
|
||||
context: Context,
|
||||
info: ProviderInfo
|
||||
) {
|
||||
super.attachInfo(context, info)
|
||||
|
||||
val processName: String = if (AndroidApiVersion.isAtLeastT) {
|
||||
getProcessNameTPlus()
|
||||
} else if (AndroidApiVersion.isAtLeastP) {
|
||||
getProcessNamePPlus()
|
||||
} else {
|
||||
getProcessNameLegacy(context, info)
|
||||
}
|
||||
val processName: String =
|
||||
if (AndroidApiVersion.isAtLeastT) {
|
||||
getProcessNameTPlus()
|
||||
} else if (AndroidApiVersion.isAtLeastP) {
|
||||
getProcessNamePPlus()
|
||||
} else {
|
||||
getProcessNameLegacy(context, info)
|
||||
}
|
||||
|
||||
ProcessNameCompat.setProcessName(processName)
|
||||
}
|
||||
|
@ -54,11 +58,18 @@ open class AbstractProcessNameContentProvider : ContentProvider() {
|
|||
throw UnsupportedOperationException()
|
||||
}
|
||||
|
||||
override fun insert(uri: Uri, values: ContentValues?): Uri? {
|
||||
override fun insert(
|
||||
uri: Uri,
|
||||
values: ContentValues?
|
||||
): Uri? {
|
||||
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()
|
||||
}
|
||||
|
||||
|
@ -72,7 +83,9 @@ open class AbstractProcessNameContentProvider : ContentProvider() {
|
|||
}
|
||||
|
||||
companion object {
|
||||
internal fun getProcessNameLegacy(context: Context, info: ProviderInfo) =
|
||||
info.processName ?: context.applicationInfo.processName ?: context.packageName
|
||||
internal fun getProcessNameLegacy(
|
||||
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.
|
||||
*/
|
||||
object ProcessNameCompat {
|
||||
|
||||
// GuardedBy intrinsicLock
|
||||
private var processName: String? = null
|
||||
|
||||
|
|
|
@ -4,7 +4,6 @@ import kotlin.test.Test
|
|||
import kotlin.test.assertFailsWith
|
||||
|
||||
class ProgressTest {
|
||||
|
||||
@Test
|
||||
fun last_greater_than_zero() {
|
||||
assertFailsWith(IllegalArgumentException::class) {
|
||||
|
|
|
@ -8,37 +8,45 @@ import java.io.File
|
|||
import java.io.IOException
|
||||
import java.util.UUID
|
||||
|
||||
suspend fun File.existsSuspend() = withContext(Dispatchers.IO) {
|
||||
exists()
|
||||
}
|
||||
suspend fun File.existsSuspend() =
|
||||
withContext(Dispatchers.IO) {
|
||||
exists()
|
||||
}
|
||||
|
||||
suspend fun File.mkdirsSuspend() = withContext(Dispatchers.IO) {
|
||||
mkdirs()
|
||||
}
|
||||
suspend fun File.mkdirsSuspend() =
|
||||
withContext(Dispatchers.IO) {
|
||||
mkdirs()
|
||||
}
|
||||
|
||||
suspend fun File.isDirectorySuspend() = withContext(Dispatchers.IO) {
|
||||
isDirectory
|
||||
}
|
||||
suspend fun File.isDirectorySuspend() =
|
||||
withContext(Dispatchers.IO) {
|
||||
isDirectory
|
||||
}
|
||||
|
||||
suspend fun File.isFileSuspend() = withContext(Dispatchers.IO) {
|
||||
isFile
|
||||
}
|
||||
suspend fun File.isFileSuspend() =
|
||||
withContext(Dispatchers.IO) {
|
||||
isFile
|
||||
}
|
||||
|
||||
suspend fun File.canWriteSuspend() = withContext(Dispatchers.IO) {
|
||||
canWrite()
|
||||
}
|
||||
suspend fun File.canWriteSuspend() =
|
||||
withContext(Dispatchers.IO) {
|
||||
canWrite()
|
||||
}
|
||||
|
||||
suspend fun File.deleteSuspend() = withContext(Dispatchers.IO) {
|
||||
delete()
|
||||
}
|
||||
suspend fun File.deleteSuspend() =
|
||||
withContext(Dispatchers.IO) {
|
||||
delete()
|
||||
}
|
||||
|
||||
suspend fun File.renameToSuspend(destination: File) = withContext(Dispatchers.IO) {
|
||||
renameTo(destination)
|
||||
}
|
||||
suspend fun File.renameToSuspend(destination: File) =
|
||||
withContext(Dispatchers.IO) {
|
||||
renameTo(destination)
|
||||
}
|
||||
|
||||
suspend fun File.listFilesSuspend() = withContext(Dispatchers.IO) {
|
||||
listFiles()
|
||||
}
|
||||
suspend fun File.listFilesSuspend() =
|
||||
withContext(Dispatchers.IO) {
|
||||
listFiles()
|
||||
}
|
||||
|
||||
/**
|
||||
* Given an ultimate output file destination, this generates a temporary file that [action] can write to. After action
|
||||
|
@ -50,18 +58,21 @@ suspend fun File.listFilesSuspend() = withContext(Dispatchers.IO) {
|
|||
* delete, rename, or do other operations in the filesystem.
|
||||
*/
|
||||
suspend fun File.writeAtomically(action: (suspend (File) -> Unit)) {
|
||||
val tempFile = withContext(Dispatchers.IO) {
|
||||
File(parentFile, name.newTempFileName()).also {
|
||||
it.deleteOnExit()
|
||||
val tempFile =
|
||||
withContext(Dispatchers.IO) {
|
||||
File(parentFile, name.newTempFileName()).also {
|
||||
it.deleteOnExit()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var isWriteSuccessful = false
|
||||
|
||||
try {
|
||||
action(tempFile)
|
||||
isWriteSuccessful = true
|
||||
} catch (@Suppress("TooGenericExceptionCaught") e: Exception) {
|
||||
} catch (
|
||||
@Suppress("TooGenericExceptionCaught") e: Exception
|
||||
) {
|
||||
tempFile.deleteSuspend()
|
||||
throw e
|
||||
} finally {
|
||||
|
|
|
@ -16,47 +16,50 @@ class WriteAtomicallyTest {
|
|||
private fun newFile() = File(File("build"), "atomic_file_test-${UUID.randomUUID()}")
|
||||
|
||||
@Test
|
||||
fun `file has temp name`() = runTest {
|
||||
val testFile = newFile()
|
||||
try {
|
||||
testFile.writeAtomically {
|
||||
it.writeText("test text")
|
||||
assertNotEquals(testFile.name, it.name)
|
||||
fun `file has temp name`() =
|
||||
runTest {
|
||||
val testFile = newFile()
|
||||
try {
|
||||
testFile.writeAtomically {
|
||||
it.writeText("test text")
|
||||
assertNotEquals(testFile.name, it.name)
|
||||
}
|
||||
} finally {
|
||||
testFile.delete()
|
||||
}
|
||||
} finally {
|
||||
testFile.delete()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `temp file deleted`() = runTest {
|
||||
val testFile = newFile()
|
||||
try {
|
||||
var tempFile: File? = null
|
||||
fun `temp file deleted`() =
|
||||
runTest {
|
||||
val testFile = newFile()
|
||||
try {
|
||||
var tempFile: File? = null
|
||||
|
||||
testFile.writeAtomically {
|
||||
tempFile = it
|
||||
it.writeText("test text")
|
||||
testFile.writeAtomically {
|
||||
tempFile = it
|
||||
it.writeText("test text")
|
||||
}
|
||||
|
||||
assertNotNull(tempFile)
|
||||
assertFalse(tempFile!!.exists())
|
||||
} finally {
|
||||
testFile.delete()
|
||||
}
|
||||
|
||||
assertNotNull(tempFile)
|
||||
assertFalse(tempFile!!.exists())
|
||||
} finally {
|
||||
testFile.delete()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `file is renamed`() = runTest {
|
||||
val testFile = newFile()
|
||||
try {
|
||||
testFile.writeAtomically {
|
||||
it.writeText("test text")
|
||||
}
|
||||
fun `file is renamed`() =
|
||||
runTest {
|
||||
val testFile = newFile()
|
||||
try {
|
||||
testFile.writeAtomically {
|
||||
it.writeText("test text")
|
||||
}
|
||||
|
||||
assertTrue(testFile.exists())
|
||||
} finally {
|
||||
testFile.delete()
|
||||
assertTrue(testFile.exists())
|
||||
} finally {
|
||||
testFile.delete()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,8 +10,9 @@ import org.junit.Before
|
|||
* 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.
|
||||
*/
|
||||
// Originally hoped to put this into ZcashUiTestRunner, although it causes reporting of test results to fail
|
||||
open class UiTestPrerequisites {
|
||||
// Originally hoped to put this into ZcashUiTestRunner, although it causes reporting of test results to fail
|
||||
|
||||
@Before
|
||||
fun verifyPrerequisites() {
|
||||
assertScreenIsOn()
|
||||
|
@ -26,8 +27,9 @@ open class UiTestPrerequisites {
|
|||
}
|
||||
|
||||
private fun isScreenOn(): Boolean {
|
||||
val powerService = ApplicationProvider.getApplicationContext<Context>()
|
||||
.getSystemService(Context.POWER_SERVICE) as PowerManager
|
||||
val powerService =
|
||||
ApplicationProvider.getApplicationContext<Context>()
|
||||
.getSystemService(Context.POWER_SERVICE) as PowerManager
|
||||
return powerService.isInteractive
|
||||
}
|
||||
|
||||
|
@ -41,7 +43,7 @@ open class UiTestPrerequisites {
|
|||
val keyguardService = (
|
||||
ApplicationProvider.getApplicationContext<Context>()
|
||||
.getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager
|
||||
)
|
||||
)
|
||||
|
||||
return keyguardService.isKeyguardLocked
|
||||
}
|
||||
|
|
|
@ -12,8 +12,9 @@ class ZcashUiTestRunner : AndroidJUnitRunner() {
|
|||
override fun onCreate(arguments: Bundle?) {
|
||||
super.onCreate(arguments)
|
||||
|
||||
val powerManager = ApplicationProvider.getApplicationContext<Context>()
|
||||
.getSystemService(Context.POWER_SERVICE) as PowerManager
|
||||
val powerManager =
|
||||
ApplicationProvider.getApplicationContext<Context>()
|
||||
.getSystemService(Context.POWER_SERVICE) as PowerManager
|
||||
|
||||
// There is no alternative to this deprecated API. The suggestion of a view to keep the screen
|
||||
// on won't work well for our tests.
|
||||
|
|
|
@ -2,4 +2,6 @@
|
|||
root = true
|
||||
[*.{kt,kts}]
|
||||
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.
|
||||
*/
|
||||
class BasicStartupBenchmark {
|
||||
|
||||
companion object {
|
||||
private const val APP_TARGET_PACKAGE_NAME = "co.electriccoin.zcash"
|
||||
}
|
||||
|
@ -27,13 +26,14 @@ class BasicStartupBenchmark {
|
|||
val benchmarkRule = MacrobenchmarkRule()
|
||||
|
||||
@Test
|
||||
fun startup() = benchmarkRule.measureRepeated(
|
||||
packageName = APP_TARGET_PACKAGE_NAME,
|
||||
metrics = listOf(StartupTimingMetric()),
|
||||
iterations = 5,
|
||||
startupMode = StartupMode.COLD
|
||||
) {
|
||||
pressHome()
|
||||
startActivityAndWait()
|
||||
}
|
||||
fun startup() =
|
||||
benchmarkRule.measureRepeated(
|
||||
packageName = APP_TARGET_PACKAGE_NAME,
|
||||
metrics = listOf(StartupTimingMetric()),
|
||||
iterations = 5,
|
||||
startupMode = StartupMode.COLD
|
||||
) {
|
||||
pressHome()
|
||||
startActivityAndWait()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -59,10 +59,11 @@ fun PrimaryButton(
|
|||
onClick: () -> Unit,
|
||||
text: String,
|
||||
modifier: Modifier = Modifier,
|
||||
outerPaddingValues: PaddingValues = PaddingValues(
|
||||
horizontal = ZcashTheme.dimens.spacingNone,
|
||||
vertical = ZcashTheme.dimens.spacingSmall
|
||||
),
|
||||
outerPaddingValues: PaddingValues =
|
||||
PaddingValues(
|
||||
horizontal = ZcashTheme.dimens.spacingNone,
|
||||
vertical = ZcashTheme.dimens.spacingSmall
|
||||
),
|
||||
enabled: Boolean = true,
|
||||
buttonColor: Color = MaterialTheme.colorScheme.primary,
|
||||
textColor: Color = MaterialTheme.colorScheme.onPrimary,
|
||||
|
@ -70,27 +71,30 @@ fun PrimaryButton(
|
|||
Button(
|
||||
shape = RectangleShape,
|
||||
enabled = enabled,
|
||||
modifier = modifier.then(Modifier.fillMaxWidth())
|
||||
.padding(outerPaddingValues)
|
||||
.shadow(
|
||||
contentColor = textColor,
|
||||
strokeColor = buttonColor,
|
||||
strokeWidth = 1.dp,
|
||||
offsetX = ZcashTheme.dimens.buttonShadowOffsetX,
|
||||
offsetY = ZcashTheme.dimens.buttonShadowOffsetY,
|
||||
spread = ZcashTheme.dimens.buttonShadowSpread,
|
||||
)
|
||||
.translationClick(
|
||||
translationX = ZcashTheme.dimens.buttonShadowOffsetX + 6.dp, // + 6dp to exactly cover the bottom shadow
|
||||
translationY = ZcashTheme.dimens.buttonShadowOffsetX + 6.dp
|
||||
)
|
||||
.defaultMinSize(ZcashTheme.dimens.buttonWidth, ZcashTheme.dimens.buttonHeight)
|
||||
.border(1.dp, Color.Black),
|
||||
colors = buttonColors(
|
||||
containerColor = buttonColor,
|
||||
disabledContainerColor = ZcashTheme.colors.disabledButtonColor,
|
||||
disabledContentColor = ZcashTheme.colors.disabledButtonTextColor
|
||||
),
|
||||
modifier =
|
||||
modifier.then(Modifier.fillMaxWidth())
|
||||
.padding(outerPaddingValues)
|
||||
.shadow(
|
||||
contentColor = textColor,
|
||||
strokeColor = buttonColor,
|
||||
strokeWidth = 1.dp,
|
||||
offsetX = ZcashTheme.dimens.buttonShadowOffsetX,
|
||||
offsetY = ZcashTheme.dimens.buttonShadowOffsetY,
|
||||
spread = ZcashTheme.dimens.buttonShadowSpread,
|
||||
)
|
||||
.translationClick(
|
||||
// + 6dp to exactly cover the bottom shadow
|
||||
translationX = ZcashTheme.dimens.buttonShadowOffsetX + 6.dp,
|
||||
translationY = ZcashTheme.dimens.buttonShadowOffsetX + 6.dp
|
||||
)
|
||||
.defaultMinSize(ZcashTheme.dimens.buttonWidth, ZcashTheme.dimens.buttonHeight)
|
||||
.border(1.dp, Color.Black),
|
||||
colors =
|
||||
buttonColors(
|
||||
containerColor = buttonColor,
|
||||
disabledContainerColor = ZcashTheme.colors.disabledButtonColor,
|
||||
disabledContentColor = ZcashTheme.colors.disabledButtonTextColor
|
||||
),
|
||||
onClick = onClick,
|
||||
) {
|
||||
Text(
|
||||
|
@ -108,10 +112,11 @@ fun SecondaryButton(
|
|||
onClick: () -> Unit,
|
||||
text: String,
|
||||
modifier: Modifier = Modifier,
|
||||
outerPaddingValues: PaddingValues = PaddingValues(
|
||||
horizontal = ZcashTheme.dimens.spacingNone,
|
||||
vertical = ZcashTheme.dimens.spacingSmall
|
||||
),
|
||||
outerPaddingValues: PaddingValues =
|
||||
PaddingValues(
|
||||
horizontal = ZcashTheme.dimens.spacingNone,
|
||||
vertical = ZcashTheme.dimens.spacingSmall
|
||||
),
|
||||
enabled: Boolean = true,
|
||||
buttonColor: Color = MaterialTheme.colorScheme.secondary,
|
||||
textColor: Color = MaterialTheme.colorScheme.onSecondary,
|
||||
|
@ -119,26 +124,29 @@ fun SecondaryButton(
|
|||
Button(
|
||||
shape = RectangleShape,
|
||||
enabled = enabled,
|
||||
modifier = modifier.then(Modifier.fillMaxWidth())
|
||||
.padding(outerPaddingValues)
|
||||
.shadow(
|
||||
contentColor = textColor,
|
||||
strokeColor = textColor,
|
||||
offsetX = ZcashTheme.dimens.buttonShadowOffsetX,
|
||||
offsetY = ZcashTheme.dimens.buttonShadowOffsetY,
|
||||
spread = ZcashTheme.dimens.buttonShadowSpread,
|
||||
)
|
||||
.translationClick(
|
||||
translationX = ZcashTheme.dimens.buttonShadowOffsetX + 6.dp, // + 6dp to exactly cover the bottom shadow
|
||||
translationY = ZcashTheme.dimens.buttonShadowOffsetX + 6.dp
|
||||
)
|
||||
.defaultMinSize(ZcashTheme.dimens.buttonWidth, ZcashTheme.dimens.buttonHeight)
|
||||
.border(1.dp, Color.Black),
|
||||
colors = buttonColors(
|
||||
containerColor = buttonColor,
|
||||
disabledContainerColor = ZcashTheme.colors.disabledButtonColor,
|
||||
disabledContentColor = ZcashTheme.colors.disabledButtonTextColor
|
||||
),
|
||||
modifier =
|
||||
modifier.then(Modifier.fillMaxWidth())
|
||||
.padding(outerPaddingValues)
|
||||
.shadow(
|
||||
contentColor = textColor,
|
||||
strokeColor = textColor,
|
||||
offsetX = ZcashTheme.dimens.buttonShadowOffsetX,
|
||||
offsetY = ZcashTheme.dimens.buttonShadowOffsetY,
|
||||
spread = ZcashTheme.dimens.buttonShadowSpread,
|
||||
)
|
||||
.translationClick(
|
||||
// + 6dp to exactly cover the bottom shadow
|
||||
translationX = ZcashTheme.dimens.buttonShadowOffsetX + 6.dp,
|
||||
translationY = ZcashTheme.dimens.buttonShadowOffsetX + 6.dp
|
||||
)
|
||||
.defaultMinSize(ZcashTheme.dimens.buttonWidth, ZcashTheme.dimens.buttonHeight)
|
||||
.border(1.dp, Color.Black),
|
||||
colors =
|
||||
buttonColors(
|
||||
containerColor = buttonColor,
|
||||
disabledContainerColor = ZcashTheme.colors.disabledButtonColor,
|
||||
disabledContentColor = ZcashTheme.colors.disabledButtonTextColor
|
||||
),
|
||||
onClick = onClick,
|
||||
) {
|
||||
Text(
|
||||
|
@ -155,18 +163,20 @@ fun NavigationButton(
|
|||
onClick: () -> Unit,
|
||||
text: String,
|
||||
modifier: Modifier = Modifier,
|
||||
outerPaddingValues: PaddingValues = PaddingValues(
|
||||
horizontal = ZcashTheme.dimens.spacingDefault,
|
||||
vertical = ZcashTheme.dimens.spacingSmall
|
||||
),
|
||||
outerPaddingValues: PaddingValues =
|
||||
PaddingValues(
|
||||
horizontal = ZcashTheme.dimens.spacingDefault,
|
||||
vertical = ZcashTheme.dimens.spacingSmall
|
||||
),
|
||||
) {
|
||||
Button(
|
||||
shape = RectangleShape,
|
||||
onClick = onClick,
|
||||
modifier = modifier.then(
|
||||
Modifier
|
||||
.padding(outerPaddingValues)
|
||||
),
|
||||
modifier =
|
||||
modifier.then(
|
||||
Modifier
|
||||
.padding(outerPaddingValues)
|
||||
),
|
||||
colors = buttonColors(containerColor = MaterialTheme.colorScheme.secondary)
|
||||
) {
|
||||
Text(
|
||||
|
@ -183,20 +193,22 @@ fun TertiaryButton(
|
|||
onClick: () -> Unit,
|
||||
text: String,
|
||||
modifier: Modifier = Modifier,
|
||||
outerPaddingValues: PaddingValues = PaddingValues(
|
||||
horizontal = ZcashTheme.dimens.spacingDefault,
|
||||
vertical = ZcashTheme.dimens.spacingSmall
|
||||
),
|
||||
outerPaddingValues: PaddingValues =
|
||||
PaddingValues(
|
||||
horizontal = ZcashTheme.dimens.spacingDefault,
|
||||
vertical = ZcashTheme.dimens.spacingSmall
|
||||
),
|
||||
enabled: Boolean = true
|
||||
) {
|
||||
Button(
|
||||
shape = RectangleShape,
|
||||
onClick = onClick,
|
||||
modifier = modifier.then(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(outerPaddingValues)
|
||||
),
|
||||
modifier =
|
||||
modifier.then(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(outerPaddingValues)
|
||||
),
|
||||
enabled = enabled,
|
||||
elevation = ButtonDefaults.buttonElevation(0.dp, 0.dp, 0.dp),
|
||||
colors = buttonColors(containerColor = ZcashTheme.colors.tertiary)
|
||||
|
@ -215,19 +227,21 @@ fun DangerousButton(
|
|||
onClick: () -> Unit,
|
||||
text: String,
|
||||
modifier: Modifier = Modifier,
|
||||
outerPaddingValues: PaddingValues = PaddingValues(
|
||||
horizontal = ZcashTheme.dimens.spacingDefault,
|
||||
vertical = ZcashTheme.dimens.spacingSmall
|
||||
),
|
||||
outerPaddingValues: PaddingValues =
|
||||
PaddingValues(
|
||||
horizontal = ZcashTheme.dimens.spacingDefault,
|
||||
vertical = ZcashTheme.dimens.spacingSmall
|
||||
),
|
||||
) {
|
||||
Button(
|
||||
shape = RectangleShape,
|
||||
onClick = onClick,
|
||||
modifier = modifier.then(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(outerPaddingValues)
|
||||
),
|
||||
modifier =
|
||||
modifier.then(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(outerPaddingValues)
|
||||
),
|
||||
colors = buttonColors(containerColor = ZcashTheme.colors.dangerous)
|
||||
) {
|
||||
Text(
|
||||
|
@ -291,6 +305,7 @@ fun Modifier.shadow(
|
|||
)
|
||||
|
||||
private enum class ButtonState { Pressed, Idle }
|
||||
|
||||
fun Modifier.translationClick(
|
||||
translationX: Dp = 0.dp,
|
||||
translationY: Dp = 0.dp
|
||||
|
@ -298,26 +313,30 @@ fun Modifier.translationClick(
|
|||
var buttonState by remember { mutableStateOf(ButtonState.Idle) }
|
||||
|
||||
val translationXAnimated by animateFloatAsState(
|
||||
targetValue = if (buttonState == ButtonState.Pressed) {
|
||||
translationX.value
|
||||
} else {
|
||||
0f
|
||||
},
|
||||
targetValue =
|
||||
if (buttonState == ButtonState.Pressed) {
|
||||
translationX.value
|
||||
} else {
|
||||
0f
|
||||
},
|
||||
label = "ClickTranslationXAnimation",
|
||||
animationSpec = tween(
|
||||
durationMillis = 100
|
||||
)
|
||||
animationSpec =
|
||||
tween(
|
||||
durationMillis = 100
|
||||
)
|
||||
)
|
||||
val translationYAnimated by animateFloatAsState(
|
||||
targetValue = if (buttonState == ButtonState.Pressed) {
|
||||
translationY.value
|
||||
} else {
|
||||
0f
|
||||
},
|
||||
targetValue =
|
||||
if (buttonState == ButtonState.Pressed) {
|
||||
translationY.value
|
||||
} else {
|
||||
0f
|
||||
},
|
||||
label = "ClickTranslationYAnimation",
|
||||
animationSpec = tween(
|
||||
durationMillis = 100
|
||||
)
|
||||
animationSpec =
|
||||
tween(
|
||||
durationMillis = 100
|
||||
)
|
||||
)
|
||||
|
||||
this
|
||||
|
@ -327,13 +346,14 @@ fun Modifier.translationClick(
|
|||
}
|
||||
.pointerInput(buttonState) {
|
||||
awaitPointerEventScope {
|
||||
buttonState = if (buttonState == ButtonState.Pressed) {
|
||||
waitForUpOrCancellation()
|
||||
ButtonState.Idle
|
||||
} else {
|
||||
awaitFirstDown(false)
|
||||
ButtonState.Pressed
|
||||
}
|
||||
buttonState =
|
||||
if (buttonState == ButtonState.Pressed) {
|
||||
waitForUpOrCancellation()
|
||||
ButtonState.Idle
|
||||
} else {
|
||||
awaitFirstDown(false)
|
||||
ButtonState.Pressed
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -41,19 +41,20 @@ fun CheckBox(
|
|||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = modifier
|
||||
) {
|
||||
val checkBoxModifier = Modifier
|
||||
.padding(
|
||||
top = ZcashTheme.dimens.spacingTiny,
|
||||
bottom = ZcashTheme.dimens.spacingTiny,
|
||||
end = ZcashTheme.dimens.spacingTiny
|
||||
)
|
||||
.then(
|
||||
if (checkBoxTestTag != null) {
|
||||
Modifier.testTag(checkBoxTestTag)
|
||||
} else {
|
||||
Modifier
|
||||
}
|
||||
)
|
||||
val checkBoxModifier =
|
||||
Modifier
|
||||
.padding(
|
||||
top = ZcashTheme.dimens.spacingTiny,
|
||||
bottom = ZcashTheme.dimens.spacingTiny,
|
||||
end = ZcashTheme.dimens.spacingTiny
|
||||
)
|
||||
.then(
|
||||
if (checkBoxTestTag != null) {
|
||||
Modifier.testTag(checkBoxTestTag)
|
||||
} else {
|
||||
Modifier
|
||||
}
|
||||
)
|
||||
val (checkedState, setCheckedState) = rememberSaveable { mutableStateOf(checked) }
|
||||
Checkbox(
|
||||
checked = checkedState,
|
||||
|
|
|
@ -85,14 +85,16 @@ fun ChipOnSurface(
|
|||
) {
|
||||
Surface(
|
||||
shape = RectangleShape,
|
||||
modifier = modifier
|
||||
.padding(horizontal = ZcashTheme.dimens.spacingTiny)
|
||||
.border(
|
||||
border = BorderStroke(
|
||||
width = ZcashTheme.dimens.chipStroke,
|
||||
color = ZcashTheme.colors.layoutStroke
|
||||
)
|
||||
),
|
||||
modifier =
|
||||
modifier
|
||||
.padding(horizontal = ZcashTheme.dimens.spacingTiny)
|
||||
.border(
|
||||
border =
|
||||
BorderStroke(
|
||||
width = ZcashTheme.dimens.chipStroke,
|
||||
color = ZcashTheme.colors.layoutStroke
|
||||
)
|
||||
),
|
||||
color = MaterialTheme.colorScheme.secondary,
|
||||
shadowElevation = ZcashTheme.dimens.chipShadowElevation,
|
||||
) {
|
||||
|
@ -100,12 +102,13 @@ fun ChipOnSurface(
|
|||
text = text,
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
color = MaterialTheme.colorScheme.onSecondary,
|
||||
modifier = Modifier
|
||||
.padding(
|
||||
vertical = ZcashTheme.dimens.spacingSmall,
|
||||
horizontal = ZcashTheme.dimens.spacingDefault
|
||||
)
|
||||
.testTag(CommonTag.CHIP)
|
||||
modifier =
|
||||
Modifier
|
||||
.padding(
|
||||
vertical = ZcashTheme.dimens.spacingSmall,
|
||||
horizontal = ZcashTheme.dimens.spacingDefault
|
||||
)
|
||||
.testTag(CommonTag.CHIP)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -48,14 +48,16 @@ fun ChipGrid(
|
|||
horizontalArrangement = Arrangement.Center
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.wrapContentWidth()
|
||||
.clickable(
|
||||
interactionSource = interactionSource,
|
||||
indication = null, // Disable ripple
|
||||
onClick = onGridClick
|
||||
)
|
||||
.testTag(CommonTag.CHIP_LAYOUT)
|
||||
modifier =
|
||||
Modifier
|
||||
.wrapContentWidth()
|
||||
.clickable(
|
||||
interactionSource = interactionSource,
|
||||
// Disable ripple
|
||||
indication = null,
|
||||
onClick = onGridClick
|
||||
)
|
||||
.testTag(CommonTag.CHIP_LAYOUT)
|
||||
) {
|
||||
wordList.chunked(CHIP_GRID_COLUMN_SIZE).forEachIndexed { chunkIndex, chunk ->
|
||||
// TODO [#1043]: Correctly align numbers and words on Recovery screen
|
||||
|
|
|
@ -9,11 +9,15 @@ import androidx.compose.ui.graphics.RectangleShape
|
|||
import co.electriccoin.zcash.ui.design.theme.ZcashTheme
|
||||
|
||||
@Composable
|
||||
fun GradientSurface(modifier: Modifier = Modifier, content: @Composable () -> Unit) {
|
||||
fun GradientSurface(
|
||||
modifier: Modifier = Modifier,
|
||||
content: @Composable () -> Unit
|
||||
) {
|
||||
Surface(
|
||||
color = Color.Transparent,
|
||||
modifier = modifier
|
||||
.background(ZcashTheme.colors.surfaceGradient()),
|
||||
modifier =
|
||||
modifier
|
||||
.background(ZcashTheme.colors.surfaceGradient()),
|
||||
shape = RectangleShape,
|
||||
content = content
|
||||
)
|
||||
|
|
|
@ -15,7 +15,10 @@ import kotlinx.coroutines.flow.StateFlow
|
|||
* for automated tests.
|
||||
*/
|
||||
@Composable
|
||||
fun Override(configurationOverrideFlow: StateFlow<ConfigurationOverride?>, content: @Composable () -> Unit) {
|
||||
fun Override(
|
||||
configurationOverrideFlow: StateFlow<ConfigurationOverride?>,
|
||||
content: @Composable () -> Unit
|
||||
) {
|
||||
val configurationOverride = configurationOverrideFlow.collectAsState().value
|
||||
|
||||
if (null == configurationOverride) {
|
||||
|
@ -23,15 +26,16 @@ fun Override(configurationOverrideFlow: StateFlow<ConfigurationOverride?>, conte
|
|||
} else {
|
||||
val configuration = configurationOverride.newConfiguration(LocalConfiguration.current)
|
||||
|
||||
val contextWrapper = run {
|
||||
val context = LocalContext.current
|
||||
object : ContextThemeWrapper() {
|
||||
init {
|
||||
attachBaseContext(context)
|
||||
applyOverrideConfiguration(configuration)
|
||||
val contextWrapper =
|
||||
run {
|
||||
val context = LocalContext.current
|
||||
object : ContextThemeWrapper() {
|
||||
init {
|
||||
attachBaseContext(context)
|
||||
applyOverrideConfiguration(configuration)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
CompositionLocalProvider(
|
||||
LocalConfiguration provides configuration,
|
||||
|
@ -43,15 +47,16 @@ fun Override(configurationOverrideFlow: StateFlow<ConfigurationOverride?>, conte
|
|||
}
|
||||
|
||||
data class ConfigurationOverride(val uiMode: UiMode?, val locale: LocaleList?) {
|
||||
fun newConfiguration(fromConfiguration: Configuration) = Configuration(fromConfiguration).apply {
|
||||
this@ConfigurationOverride.uiMode?.let {
|
||||
uiMode = (uiMode and Configuration.UI_MODE_NIGHT_MASK.inv()) or it.flag()
|
||||
}
|
||||
fun newConfiguration(fromConfiguration: Configuration) =
|
||||
Configuration(fromConfiguration).apply {
|
||||
this@ConfigurationOverride.uiMode?.let {
|
||||
uiMode = (uiMode and Configuration.UI_MODE_NIGHT_MASK.inv()) or it.flag()
|
||||
}
|
||||
|
||||
this@ConfigurationOverride.locale?.let {
|
||||
setLocales(it)
|
||||
this@ConfigurationOverride.locale?.let {
|
||||
setLocales(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum class UiMode {
|
||||
|
@ -59,7 +64,8 @@ enum class UiMode {
|
|||
Dark
|
||||
}
|
||||
|
||||
private fun UiMode.flag() = when (this) {
|
||||
UiMode.Light -> Configuration.UI_MODE_NIGHT_NO
|
||||
UiMode.Dark -> Configuration.UI_MODE_NIGHT_YES
|
||||
}
|
||||
private fun UiMode.flag() =
|
||||
when (this) {
|
||||
UiMode.Light -> Configuration.UI_MODE_NIGHT_NO
|
||||
UiMode.Dark -> Configuration.UI_MODE_NIGHT_YES
|
||||
}
|
||||
|
|
|
@ -24,48 +24,53 @@ fun SwitchWithLabel(
|
|||
val interactionSource = remember { MutableInteractionSource() }
|
||||
|
||||
ConstraintLayout(
|
||||
modifier = modifier
|
||||
.clickable(
|
||||
interactionSource = interactionSource,
|
||||
indication = null, // disable ripple
|
||||
role = Role.Switch,
|
||||
onClick = { onStateChange(!state) }
|
||||
)
|
||||
.fillMaxWidth()
|
||||
modifier =
|
||||
modifier
|
||||
.clickable(
|
||||
interactionSource = interactionSource,
|
||||
// disable ripple
|
||||
indication = null,
|
||||
role = Role.Switch,
|
||||
onClick = { onStateChange(!state) }
|
||||
)
|
||||
.fillMaxWidth()
|
||||
) {
|
||||
val (text, spacer, switchButton) = createRefs()
|
||||
Body(
|
||||
text = label,
|
||||
modifier = Modifier.constrainAs(text) {
|
||||
top.linkTo(parent.top)
|
||||
bottom.linkTo(parent.top)
|
||||
start.linkTo(parent.start)
|
||||
end.linkTo(spacer.start)
|
||||
width = Dimension.fillToConstraints
|
||||
}
|
||||
)
|
||||
Spacer(
|
||||
modifier = Modifier
|
||||
.width(ZcashTheme.dimens.spacingDefault)
|
||||
.constrainAs(spacer) {
|
||||
modifier =
|
||||
Modifier.constrainAs(text) {
|
||||
top.linkTo(parent.top)
|
||||
bottom.linkTo(parent.top)
|
||||
start.linkTo(text.end)
|
||||
end.linkTo(switchButton.start)
|
||||
start.linkTo(parent.start)
|
||||
end.linkTo(spacer.start)
|
||||
width = Dimension.fillToConstraints
|
||||
}
|
||||
)
|
||||
Spacer(
|
||||
modifier =
|
||||
Modifier
|
||||
.width(ZcashTheme.dimens.spacingDefault)
|
||||
.constrainAs(spacer) {
|
||||
top.linkTo(parent.top)
|
||||
bottom.linkTo(parent.top)
|
||||
start.linkTo(text.end)
|
||||
end.linkTo(switchButton.start)
|
||||
}
|
||||
)
|
||||
Switch(
|
||||
checked = state,
|
||||
onCheckedChange = {
|
||||
onStateChange(it)
|
||||
},
|
||||
modifier = Modifier.constrainAs(switchButton) {
|
||||
top.linkTo(parent.top)
|
||||
bottom.linkTo(parent.top)
|
||||
start.linkTo(spacer.end)
|
||||
end.linkTo(parent.end)
|
||||
width = Dimension.wrapContent
|
||||
}
|
||||
modifier =
|
||||
Modifier.constrainAs(switchButton) {
|
||||
top.linkTo(parent.top)
|
||||
bottom.linkTo(parent.top)
|
||||
start.linkTo(spacer.end)
|
||||
end.linkTo(parent.end)
|
||||
width = Dimension.wrapContent
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -147,21 +147,23 @@ fun Reference(
|
|||
onClick: () -> Unit
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.wrapContentSize()
|
||||
.clip(RoundedCornerShape(ZcashTheme.dimens.topAppBarActionRippleCorner))
|
||||
.clickable { onClick() }
|
||||
modifier =
|
||||
Modifier
|
||||
.wrapContentSize()
|
||||
.clip(RoundedCornerShape(ZcashTheme.dimens.topAppBarActionRippleCorner))
|
||||
.clickable { onClick() }
|
||||
) {
|
||||
Text(
|
||||
text = text,
|
||||
style = MaterialTheme.typography.bodyLarge
|
||||
.merge(
|
||||
TextStyle(
|
||||
color = ZcashTheme.colors.reference,
|
||||
textAlign = textAlign,
|
||||
textDecoration = TextDecoration.Underline
|
||||
)
|
||||
),
|
||||
style =
|
||||
MaterialTheme.typography.bodyLarge
|
||||
.merge(
|
||||
TextStyle(
|
||||
color = ZcashTheme.colors.reference,
|
||||
textAlign = textAlign,
|
||||
textDecoration = TextDecoration.Underline
|
||||
)
|
||||
),
|
||||
modifier = modifier
|
||||
)
|
||||
}
|
||||
|
|
|
@ -27,15 +27,17 @@ fun FormTextField(
|
|||
leadingIcon: @Composable (() -> Unit)? = null,
|
||||
trailingIcon: @Composable (() -> Unit)? = null,
|
||||
keyboardOptions: KeyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Text),
|
||||
colors: TextFieldColors = TextFieldDefaults.colors(
|
||||
focusedContainerColor = Color.Transparent,
|
||||
unfocusedContainerColor = Color.Transparent,
|
||||
disabledContainerColor = Color.Transparent,
|
||||
errorContainerColor = Color.Transparent,
|
||||
),
|
||||
colors: TextFieldColors =
|
||||
TextFieldDefaults.colors(
|
||||
focusedContainerColor = Color.Transparent,
|
||||
unfocusedContainerColor = Color.Transparent,
|
||||
disabledContainerColor = Color.Transparent,
|
||||
errorContainerColor = Color.Transparent,
|
||||
),
|
||||
keyboardActions: KeyboardActions = KeyboardActions.Default,
|
||||
shape: Shape = TextFieldDefaults.shape,
|
||||
withBorder: Boolean = true, // To enable border around the TextField
|
||||
// To enable border around the TextField
|
||||
withBorder: Boolean = true,
|
||||
) {
|
||||
TextField(
|
||||
value = value,
|
||||
|
@ -44,13 +46,14 @@ fun FormTextField(
|
|||
textStyle = textStyle,
|
||||
keyboardOptions = keyboardOptions,
|
||||
colors = colors,
|
||||
modifier = modifier.then(
|
||||
if (withBorder) {
|
||||
modifier.border(width = 1.dp, color = MaterialTheme.colorScheme.primary)
|
||||
} else {
|
||||
Modifier
|
||||
}
|
||||
),
|
||||
modifier =
|
||||
modifier.then(
|
||||
if (withBorder) {
|
||||
modifier.border(width = 1.dp, color = MaterialTheme.colorScheme.primary)
|
||||
} else {
|
||||
Modifier
|
||||
}
|
||||
),
|
||||
leadingIcon = leadingIcon,
|
||||
trailingIcon = trailingIcon,
|
||||
keyboardActions = keyboardActions,
|
||||
|
|
|
@ -181,9 +181,10 @@ private fun TopBarOneVisibleActionMenuExample(
|
|||
text = "Action 1",
|
||||
onClick = actionCallback,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = modifier.then(
|
||||
Modifier.padding(all = ZcashTheme.dimens.spacingDefault)
|
||||
)
|
||||
modifier =
|
||||
modifier.then(
|
||||
Modifier.padding(all = ZcashTheme.dimens.spacingDefault)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -218,10 +219,11 @@ fun SmallTopAppBar(
|
|||
navigationIcon = {
|
||||
backText?.let {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.wrapContentSize()
|
||||
.clip(RoundedCornerShape(ZcashTheme.dimens.topAppBarActionRippleCorner))
|
||||
.clickable { onBack?.run { onBack() } }
|
||||
modifier =
|
||||
Modifier
|
||||
.wrapContentSize()
|
||||
.clip(RoundedCornerShape(ZcashTheme.dimens.topAppBarActionRippleCorner))
|
||||
.clickable { onBack?.run { onBack() } }
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.padding(all = ZcashTheme.dimens.spacingDefault),
|
||||
|
|
|
@ -18,30 +18,23 @@ data class Dimens(
|
|||
val spacingLarge: Dp,
|
||||
val spacingXlarge: Dp,
|
||||
val spacingHuge: Dp,
|
||||
|
||||
// List of custom spacings:
|
||||
|
||||
// Button:
|
||||
val buttonShadowOffsetX: Dp,
|
||||
val buttonShadowOffsetY: Dp,
|
||||
val buttonShadowSpread: Dp,
|
||||
val buttonWidth: Dp,
|
||||
val buttonHeight: Dp,
|
||||
|
||||
// Chip
|
||||
val chipShadowElevation: Dp,
|
||||
val chipStroke: Dp,
|
||||
|
||||
// TopAppBar:
|
||||
val topAppBarZcashLogoHeight: Dp,
|
||||
val topAppBarActionRippleCorner: Dp,
|
||||
|
||||
// TextField:
|
||||
val textFieldDefaultHeight: Dp,
|
||||
|
||||
// Any Layout:
|
||||
val layoutStroke: Dp,
|
||||
|
||||
// Screen custom spacings:
|
||||
val inScreenZcashLogoHeight: Dp,
|
||||
val inScreenZcashLogoWidth: Dp,
|
||||
|
@ -49,35 +42,36 @@ data class Dimens(
|
|||
val screenHorizontalSpacing: Dp,
|
||||
)
|
||||
|
||||
private val defaultDimens = Dimens(
|
||||
spacingNone = 0.dp,
|
||||
spacingXtiny = 2.dp,
|
||||
spacingTiny = 4.dp,
|
||||
spacingSmall = 8.dp,
|
||||
spacingDefault = 16.dp,
|
||||
spacingLarge = 24.dp,
|
||||
spacingXlarge = 32.dp,
|
||||
spacingHuge = 64.dp,
|
||||
buttonShadowOffsetX = 20.dp,
|
||||
buttonShadowOffsetY = 20.dp,
|
||||
buttonShadowSpread = 10.dp,
|
||||
buttonWidth = 230.dp,
|
||||
buttonHeight = 50.dp,
|
||||
chipShadowElevation = 4.dp,
|
||||
chipStroke = 0.5.dp,
|
||||
topAppBarZcashLogoHeight = 24.dp,
|
||||
topAppBarActionRippleCorner = 28.dp,
|
||||
textFieldDefaultHeight = 215.dp,
|
||||
layoutStroke = 1.dp,
|
||||
inScreenZcashLogoHeight = 100.dp,
|
||||
inScreenZcashLogoWidth = 60.dp,
|
||||
inScreenZcashTextLogoHeight = 30.dp,
|
||||
screenHorizontalSpacing = 64.dp,
|
||||
)
|
||||
private val defaultDimens =
|
||||
Dimens(
|
||||
spacingNone = 0.dp,
|
||||
spacingXtiny = 2.dp,
|
||||
spacingTiny = 4.dp,
|
||||
spacingSmall = 8.dp,
|
||||
spacingDefault = 16.dp,
|
||||
spacingLarge = 24.dp,
|
||||
spacingXlarge = 32.dp,
|
||||
spacingHuge = 64.dp,
|
||||
buttonShadowOffsetX = 20.dp,
|
||||
buttonShadowOffsetY = 20.dp,
|
||||
buttonShadowSpread = 10.dp,
|
||||
buttonWidth = 230.dp,
|
||||
buttonHeight = 50.dp,
|
||||
chipShadowElevation = 4.dp,
|
||||
chipStroke = 0.5.dp,
|
||||
topAppBarZcashLogoHeight = 24.dp,
|
||||
topAppBarActionRippleCorner = 28.dp,
|
||||
textFieldDefaultHeight = 215.dp,
|
||||
layoutStroke = 1.dp,
|
||||
inScreenZcashLogoHeight = 100.dp,
|
||||
inScreenZcashLogoWidth = 60.dp,
|
||||
inScreenZcashTextLogoHeight = 30.dp,
|
||||
screenHorizontalSpacing = 64.dp,
|
||||
)
|
||||
|
||||
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
|
||||
|
@ -119,7 +113,7 @@ internal var LocalDimens = staticCompositionLocalOf { defaultDimens }
|
|||
* - rounded/normal screen shape
|
||||
*/
|
||||
@Composable
|
||||
internal fun ProvideDimens(content: @Composable () -> Unit,) {
|
||||
internal fun ProvideDimens(content: @Composable () -> Unit) {
|
||||
val resultDimens = normalDimens
|
||||
CompositionLocalProvider(LocalDimens provides resultDimens, content = content)
|
||||
CompositionLocalProvider(localDimens provides resultDimens, content = content)
|
||||
}
|
||||
|
|
|
@ -37,10 +37,12 @@ data class ExtendedColors(
|
|||
val welcomeAnimationColor: Color,
|
||||
) {
|
||||
@Composable
|
||||
fun surfaceGradient() = Brush.verticalGradient(
|
||||
colors = listOf(
|
||||
MaterialTheme.colorScheme.surface,
|
||||
ZcashTheme.colors.surfaceEnd
|
||||
fun surfaceGradient() =
|
||||
Brush.verticalGradient(
|
||||
colors =
|
||||
listOf(
|
||||
MaterialTheme.colorScheme.surface,
|
||||
ZcashTheme.colors.surfaceEnd
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
|
|
@ -31,17 +31,19 @@ fun ZcashTheme(
|
|||
// 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 baseColors = if (useDarkMode) {
|
||||
DarkColorPalette
|
||||
} else {
|
||||
LightColorPalette
|
||||
}
|
||||
val baseColors =
|
||||
if (useDarkMode) {
|
||||
DarkColorPalette
|
||||
} else {
|
||||
LightColorPalette
|
||||
}
|
||||
|
||||
val extendedColors = if (useDarkMode) {
|
||||
DarkExtendedColorPalette
|
||||
} else {
|
||||
LightExtendedColorPalette
|
||||
}
|
||||
val extendedColors =
|
||||
if (useDarkMode) {
|
||||
DarkExtendedColorPalette
|
||||
} else {
|
||||
LightExtendedColorPalette
|
||||
}
|
||||
|
||||
CompositionLocalProvider(LocalExtendedColors provides extendedColors) {
|
||||
ProvideDimens {
|
||||
|
@ -72,5 +74,5 @@ object ZcashTheme {
|
|||
// TODO [#808]: https://github.com/Electric-Coin-Company/zashi-android/issues/808
|
||||
val dimens: Dimens
|
||||
@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