[#1108] Ktlint 1.0.1

* [#1108] Ktlint 1.0.1

- Closes #1108
- Version and artefact update

* Fix ktlint warnings
This commit is contained in:
Honza Rychnovský 2023-12-11 10:20:32 +01:00 committed by GitHub
parent a10b372e73
commit a3e7d8f6c4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
257 changed files with 4229 additions and 3516 deletions

View File

@ -10,7 +10,6 @@ import org.junit.Assert.assertEquals
import org.junit.Test import org.junit.Test
class AndroidApiTest { class AndroidApiTest {
@Test @Test
@SmallTest @SmallTest
fun checkTargetApi() { fun checkTargetApi() {

View File

@ -9,7 +9,6 @@ import kotlinx.coroutines.launch
@Suppress("unused") @Suppress("unused")
class ZcashApplication : CoroutineApplication() { class ZcashApplication : CoroutineApplication() {
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()

View File

@ -5,7 +5,7 @@ plugins {
val ktlint by configurations.creating val ktlint by configurations.creating
dependencies { dependencies {
ktlint("com.pinterest:ktlint:${project.property("KTLINT_VERSION")}") { ktlint("com.pinterest.ktlint:ktlint-cli:${project.property("KTLINT_VERSION")}") {
attributes { attributes {
attribute(Bundling.BUNDLING_ATTRIBUTE, objects.named<Bundling>(Bundling.EXTERNAL)) attribute(Bundling.BUNDLING_ATTRIBUTE, objects.named<Bundling>(Bundling.EXTERNAL))
} }

View File

@ -7,7 +7,6 @@ import kotlinx.coroutines.flow.Flow
* Provides a remote config implementation. * Provides a remote config implementation.
*/ */
interface ConfigurationProvider { interface ConfigurationProvider {
/** /**
* @return The configuration if it has been loaded already. If not loaded, returns an empty configuration. * @return The configuration if it has been loaded already. If not loaded, returns an empty configuration.
*/ */

View File

@ -40,19 +40,28 @@ private data class MergingConfiguration(private val configurations: PersistentLi
return null != configurations.firstWithKey(key) return null != configurations.firstWithKey(key)
} }
override fun getBoolean(key: ConfigKey, defaultValue: Boolean): Boolean { override fun getBoolean(
key: ConfigKey,
defaultValue: Boolean
): Boolean {
return configurations.firstWithKey(key)?.let { return configurations.firstWithKey(key)?.let {
return it.getBoolean(key, defaultValue) return it.getBoolean(key, defaultValue)
} ?: defaultValue } ?: defaultValue
} }
override fun getInt(key: ConfigKey, defaultValue: Int): Int { override fun getInt(
key: ConfigKey,
defaultValue: Int
): Int {
return configurations.firstWithKey(key)?.let { return configurations.firstWithKey(key)?.let {
return it.getInt(key, defaultValue) return it.getInt(key, defaultValue)
} ?: defaultValue } ?: defaultValue
} }
override fun getString(key: ConfigKey, defaultValue: String): String { override fun getString(
key: ConfigKey,
defaultValue: String
): String {
return configurations.firstWithKey(key)?.let { return configurations.firstWithKey(key)?.let {
return it.getString(key, defaultValue) return it.getString(key, defaultValue)
} ?: defaultValue } ?: defaultValue

View File

@ -6,7 +6,5 @@ data class BooleanConfigurationEntry(
override val key: ConfigKey, override val key: ConfigKey,
private val defaultValue: Boolean private val defaultValue: Boolean
) : DefaultEntry<Boolean> { ) : DefaultEntry<Boolean> {
override fun getValue(configuration: Configuration) = configuration.getBoolean(key, defaultValue)
override fun getValue(configuration: Configuration) =
configuration.getBoolean(key, defaultValue)
} }

View File

@ -8,6 +8,7 @@ import co.electriccoin.zcash.configuration.model.map.Configuration
* variation in default value. Clients define the key and default value together, rather than just * variation in default value. Clients define the key and default value together, rather than just
* the key. * the key.
*/ */
interface DefaultEntry<T> {
/* /*
* API note: the default value is not available through the public interface in order to prevent * API note: the default value is not available through the public interface in order to prevent
* clients from accidentally using the default value instead of the configuration value. * clients from accidentally using the default value instead of the configuration value.
@ -17,7 +18,6 @@ import co.electriccoin.zcash.configuration.model.map.Configuration
* Overall the number of Integer configuration entries is expected to be low compared to Booleans, * Overall the number of Integer configuration entries is expected to be low compared to Booleans,
* and perhaps many Integer values will also fit within the autoboxing cache. * and perhaps many Integer values will also fit within the autoboxing cache.
*/ */
interface DefaultEntry<T> {
val key: ConfigKey val key: ConfigKey

View File

@ -6,6 +6,5 @@ data class IntegerConfigurationEntry(
override val key: ConfigKey, override val key: ConfigKey,
private val defaultValue: Int private val defaultValue: Int
) : DefaultEntry<Int> { ) : DefaultEntry<Int> {
override fun getValue(configuration: Configuration) = configuration.getInt(key, defaultValue) override fun getValue(configuration: Configuration) = configuration.getInt(key, defaultValue)
} }

View File

@ -6,6 +6,5 @@ data class StringConfigurationEntry(
override val key: ConfigKey, override val key: ConfigKey,
private val defaultValue: String private val defaultValue: String
) : DefaultEntry<String> { ) : DefaultEntry<String> {
override fun getValue(configuration: Configuration) = configuration.getString(key, defaultValue) override fun getValue(configuration: Configuration) = configuration.getString(key, defaultValue)
} }

View File

@ -26,7 +26,10 @@ interface Configuration {
* be returned if type coercion fails. * be returned if type coercion fails.
* @return boolean mapping for `key` or `defaultValue`. * @return boolean mapping for `key` or `defaultValue`.
*/ */
fun getBoolean(key: ConfigKey, defaultValue: Boolean): Boolean fun getBoolean(
key: ConfigKey,
defaultValue: Boolean
): Boolean
/** /**
* @param key Key to use to retrieve the value. * @param key Key to use to retrieve the value.
@ -35,7 +38,10 @@ interface Configuration {
* be returned if type coercion fails. * be returned if type coercion fails.
* @return int mapping for `key` or `defaultValue`. * @return int mapping for `key` or `defaultValue`.
*/ */
fun getInt(key: ConfigKey, defaultValue: Int): Int fun getInt(
key: ConfigKey,
defaultValue: Int
): Int
/** /**
* @param key Key to use to retrieve the value. * @param key Key to use to retrieve the value.
@ -44,5 +50,8 @@ interface Configuration {
* be returned if type coercion fails. * be returned if type coercion fails.
* @return String mapping for `key` or `defaultValue`. * @return String mapping for `key` or `defaultValue`.
*/ */
fun getString(key: ConfigKey, defaultValue: String): String fun getString(
key: ConfigKey,
defaultValue: String
): String
} }

View File

@ -10,30 +10,38 @@ data class StringConfiguration(
val configurationMapping: PersistentMap<String, String>, val configurationMapping: PersistentMap<String, String>,
override val updatedAt: Instant? override val updatedAt: Instant?
) : Configuration { ) : Configuration {
override fun getBoolean( override fun getBoolean(
key: ConfigKey, key: ConfigKey,
defaultValue: Boolean defaultValue: Boolean
) = configurationMapping[key.key]?.let { ) = configurationMapping[key.key]?.let {
try { try {
it.toBooleanStrict() it.toBooleanStrict()
} catch (@Suppress("SwallowedException") e: IllegalArgumentException) { } catch (
@Suppress("SwallowedException") e: IllegalArgumentException
) {
// In the future, log coercion failure as this could mean someone made an error in the remote config console // In the future, log coercion failure as this could mean someone made an error in the remote config console
defaultValue defaultValue
} }
} ?: defaultValue } ?: defaultValue
override fun getInt(key: ConfigKey, defaultValue: Int) = configurationMapping[key.key]?.let { override fun getInt(
key: ConfigKey,
defaultValue: Int
) = configurationMapping[key.key]?.let {
try { try {
it.toInt() it.toInt()
} catch (@Suppress("SwallowedException") e: NumberFormatException) { } catch (
@Suppress("SwallowedException") e: NumberFormatException
) {
// In the future, log coercion failure as this could mean someone made an error in the remote config console // In the future, log coercion failure as this could mean someone made an error in the remote config console
defaultValue defaultValue
} }
} ?: defaultValue } ?: defaultValue
override fun getString(key: ConfigKey, defaultValue: String) = override fun getString(
configurationMapping.getOrElse(key.key) { defaultValue } key: ConfigKey,
defaultValue: String
) = configurationMapping.getOrElse(key.key) { defaultValue }
override fun hasKey(key: ConfigKey) = configurationMapping.containsKey(key.key) override fun hasKey(key: ConfigKey) = configurationMapping.containsKey(key.key)
} }

View File

@ -18,13 +18,20 @@ import kotlin.test.assertTrue
class MergingConfigurationProviderTest { class MergingConfigurationProviderTest {
@Test @Test
fun peek_ordering() { fun peek_ordering() {
val configurationProvider = MergingConfigurationProvider( val configurationProvider =
MergingConfigurationProvider(
persistentListOf( persistentListOf(
MockConfigurationProvider( MockConfigurationProvider(
StringConfiguration(persistentMapOf(BooleanDefaultEntryFixture.KEY.key to true.toString()), null) StringConfiguration(
persistentMapOf(BooleanDefaultEntryFixture.KEY.key to true.toString()),
null
)
), ),
MockConfigurationProvider( MockConfigurationProvider(
StringConfiguration(persistentMapOf(BooleanDefaultEntryFixture.KEY.key to false.toString()), null) StringConfiguration(
persistentMapOf(BooleanDefaultEntryFixture.KEY.key to false.toString()),
null
)
) )
) )
) )
@ -33,14 +40,22 @@ class MergingConfigurationProviderTest {
} }
@Test @Test
fun getFlow_ordering() = runTest { fun getFlow_ordering() =
val configurationProvider = MergingConfigurationProvider( runTest {
val configurationProvider =
MergingConfigurationProvider(
persistentListOf( persistentListOf(
MockConfigurationProvider( MockConfigurationProvider(
StringConfiguration(persistentMapOf(BooleanDefaultEntryFixture.KEY.key to true.toString()), null) StringConfiguration(
persistentMapOf(BooleanDefaultEntryFixture.KEY.key to true.toString()),
null
)
), ),
MockConfigurationProvider( MockConfigurationProvider(
StringConfiguration(persistentMapOf(BooleanDefaultEntryFixture.KEY.key to false.toString()), null) StringConfiguration(
persistentMapOf(BooleanDefaultEntryFixture.KEY.key to false.toString()),
null
)
) )
) )
) )
@ -51,8 +66,10 @@ class MergingConfigurationProviderTest {
} }
@Test @Test
fun getFlow_empty() = runTest { fun getFlow_empty() =
val configurationProvider = MergingConfigurationProvider( runTest {
val configurationProvider =
MergingConfigurationProvider(
emptyList<ConfigurationProvider>().toPersistentList() emptyList<ConfigurationProvider>().toPersistentList()
) )
@ -62,17 +79,27 @@ class MergingConfigurationProviderTest {
} }
@Test @Test
fun getUpdatedAt_newest() = runTest { fun getUpdatedAt_newest() =
runTest {
val older = "2023-01-15T08:38:45.415Z".toInstant() val older = "2023-01-15T08:38:45.415Z".toInstant()
val newer = "2023-01-17T08:38:45.415Z".toInstant() val newer = "2023-01-17T08:38:45.415Z".toInstant()
val configurationProvider = MergingConfigurationProvider( val configurationProvider =
MergingConfigurationProvider(
persistentListOf( persistentListOf(
MockConfigurationProvider( MockConfigurationProvider(
StringConfiguration(persistentMapOf(BooleanDefaultEntryFixture.KEY.key to true.toString()), older) StringConfiguration(
persistentMapOf(BooleanDefaultEntryFixture.KEY.key to true.toString()),
older
)
), ),
MockConfigurationProvider( MockConfigurationProvider(
StringConfiguration(persistentMapOf(BooleanDefaultEntryFixture.KEY.key to false.toString()), newer) StringConfiguration(
persistentMapOf(
BooleanDefaultEntryFixture.KEY.key to false.toString()
),
newer
)
) )
) )
) )
@ -83,7 +110,6 @@ class MergingConfigurationProviderTest {
} }
private class MockConfigurationProvider(private val configuration: Configuration) : ConfigurationProvider { private class MockConfigurationProvider(private val configuration: Configuration) : ConfigurationProvider {
override fun peekConfiguration(): Configuration { override fun peekConfiguration(): Configuration {
return configuration return configuration
} }

View File

@ -11,7 +11,6 @@ import kotlinx.datetime.Instant
* though, making the initial mapping thread-safe. * though, making the initial mapping thread-safe.
*/ */
class MockConfiguration(private val configurationMapping: Map<String, String> = emptyMap()) : Configuration { class MockConfiguration(private val configurationMapping: Map<String, String> = emptyMap()) : Configuration {
override val updatedAt: Instant? = null override val updatedAt: Instant? = null
override fun getBoolean( override fun getBoolean(
@ -20,23 +19,32 @@ class MockConfiguration(private val configurationMapping: Map<String, String> =
) = configurationMapping[key.key]?.let { ) = configurationMapping[key.key]?.let {
try { try {
it.toBooleanStrict() it.toBooleanStrict()
} catch (@Suppress("SwallowedException") e: IllegalArgumentException) { } catch (
@Suppress("SwallowedException") e: IllegalArgumentException
) {
// In the future, log coercion failure as this could mean someone made an error in the remote config console // In the future, log coercion failure as this could mean someone made an error in the remote config console
defaultValue defaultValue
} }
} ?: defaultValue } ?: defaultValue
override fun getInt(key: ConfigKey, defaultValue: Int) = configurationMapping[key.key]?.let { override fun getInt(
key: ConfigKey,
defaultValue: Int
) = configurationMapping[key.key]?.let {
try { try {
it.toInt() it.toInt()
} catch (@Suppress("SwallowedException") e: NumberFormatException) { } catch (
@Suppress("SwallowedException") e: NumberFormatException
) {
// In the future, log coercion failure as this could mean someone made an error in the remote config console // In the future, log coercion failure as this could mean someone made an error in the remote config console
defaultValue defaultValue
} }
} ?: defaultValue } ?: defaultValue
override fun getString(key: ConfigKey, defaultValue: String) = override fun getString(
configurationMapping.getOrElse(key.key) { defaultValue } key: ConfigKey,
defaultValue: String
) = configurationMapping.getOrElse(key.key) { defaultValue }
override fun hasKey(key: ConfigKey) = configurationMapping.containsKey(key.key) override fun hasKey(key: ConfigKey) = configurationMapping.containsKey(key.key)
} }

View File

@ -4,7 +4,6 @@ import co.electriccoin.zcash.configuration.model.entry.BooleanConfigurationEntry
import co.electriccoin.zcash.configuration.model.entry.ConfigKey import co.electriccoin.zcash.configuration.model.entry.ConfigKey
object BooleanDefaultEntryFixture { object BooleanDefaultEntryFixture {
val KEY = ConfigKey("some_boolean_key") // $NON-NLS val KEY = ConfigKey("some_boolean_key") // $NON-NLS
fun newTrueEntry() = BooleanConfigurationEntry(KEY, true) fun newTrueEntry() = BooleanConfigurationEntry(KEY, true)

View File

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

View File

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

View File

@ -8,8 +8,8 @@ import co.electriccoin.zcash.spackle.LazyWithArgument
import kotlinx.collections.immutable.toPersistentList import kotlinx.collections.immutable.toPersistentList
object AndroidConfigurationFactory { object AndroidConfigurationFactory {
private val instance =
private val instance = LazyWithArgument<Context, ConfigurationProvider> { context -> LazyWithArgument<Context, ConfigurationProvider> { context ->
new(context) new(context)
} }
@ -17,8 +17,11 @@ object AndroidConfigurationFactory {
// Context will be needed for most cloud providers, e.g. to integrate with Firebase or other // Context will be needed for most cloud providers, e.g. to integrate with Firebase or other
// remote configuration providers. // remote configuration providers.
private fun new(@Suppress("UNUSED_PARAMETER") context: Context): ConfigurationProvider { private fun new(
val configurationProviders = buildList<ConfigurationProvider> { @Suppress("UNUSED_PARAMETER") context: Context
): ConfigurationProvider {
val configurationProviders =
buildList<ConfigurationProvider> {
// For ordering, ensure the IntentConfigurationProvider is first so that it can // For ordering, ensure the IntentConfigurationProvider is first so that it can
// override any other configuration providers. // override any other configuration providers.
if (BuildConfig.DEBUG) { if (BuildConfig.DEBUG) {

View File

@ -8,7 +8,6 @@ import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
internal object IntentConfigurationProvider : ConfigurationProvider { internal object IntentConfigurationProvider : ConfigurationProvider {
private val configurationStateFlow = MutableStateFlow(StringConfiguration(persistentMapOf(), null)) private val configurationStateFlow = MutableStateFlow(StringConfiguration(persistentMapOf(), null))
override fun peekConfiguration() = configurationStateFlow.value override fun peekConfiguration() = configurationStateFlow.value

View File

@ -8,14 +8,18 @@ import kotlinx.collections.immutable.toPersistentMap
import kotlinx.datetime.Clock import kotlinx.datetime.Clock
class IntentConfigurationReceiver : BroadcastReceiver() { class IntentConfigurationReceiver : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) { override fun onReceive(
context: Context?,
intent: Intent?
) {
intent?.defuse()?.let { intent?.defuse()?.let {
val key = it.getStringExtra(ConfigurationIntent.EXTRA_STRING_KEY) val key = it.getStringExtra(ConfigurationIntent.EXTRA_STRING_KEY)
val value = it.getStringExtra(ConfigurationIntent.EXTRA_STRING_VALUE) val value = it.getStringExtra(ConfigurationIntent.EXTRA_STRING_VALUE)
if (null != key) { if (null != key) {
val existingConfiguration = IntentConfigurationProvider.peekConfiguration().configurationMapping val existingConfiguration = IntentConfigurationProvider.peekConfiguration().configurationMapping
val newConfiguration = if (null == value) { val newConfiguration =
if (null == value) {
existingConfiguration.remove(key) existingConfiguration.remove(key)
} else { } else {
existingConfiguration + (key to value) existingConfiguration + (key to value)
@ -34,7 +38,9 @@ private fun Intent.defuse(): Intent? {
return try { return try {
extras?.containsKey(null) extras?.containsKey(null)
this this
} catch (@Suppress("SwallowedException", "TooGenericExceptionCaught") e: Exception) { } catch (
@Suppress("SwallowedException", "TooGenericExceptionCaught") e: Exception
) {
null null
} }
} }

View File

@ -9,7 +9,6 @@ import java.util.concurrent.CountDownLatch
import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.atomic.AtomicBoolean
class AndroidUncaughtExceptionHandlerTest { class AndroidUncaughtExceptionHandlerTest {
@Test(expected = IllegalStateException::class) @Test(expected = IllegalStateException::class)
fun requires_main_thread() { fun requires_main_thread() {
AndroidUncaughtExceptionHandler.register(ApplicationProvider.getApplicationContext()) AndroidUncaughtExceptionHandler.register(ApplicationProvider.getApplicationContext())

View File

@ -12,7 +12,6 @@ import org.junit.Assert.assertTrue
import org.junit.Test import org.junit.Test
class Components { class Components {
@Test @Test
@SmallTest @SmallTest
fun process_names() { fun process_names() {
@ -26,14 +25,16 @@ class Components {
} }
} }
private fun PackageManager.getProviderInfoCompat(componentName: ComponentName) = if (AndroidApiVersion.isAtLeastT) { private fun PackageManager.getProviderInfoCompat(componentName: ComponentName) =
if (AndroidApiVersion.isAtLeastT) {
getProviderInfo(componentName, PackageManager.ComponentInfoFlags.of(0)) getProviderInfo(componentName, PackageManager.ComponentInfoFlags.of(0))
} else { } else {
@Suppress("Deprecation") @Suppress("Deprecation")
getProviderInfo(componentName, 0) getProviderInfo(componentName, 0)
} }
private fun PackageManager.getReceiverInfoCompat(componentName: ComponentName) = if (AndroidApiVersion.isAtLeastT) { private fun PackageManager.getReceiverInfoCompat(componentName: ComponentName) =
if (AndroidApiVersion.isAtLeastT) {
getReceiverInfo(componentName, PackageManager.ComponentInfoFlags.of(0)) getReceiverInfo(componentName, PackageManager.ComponentInfoFlags.of(0))
} else { } else {
@Suppress("Deprecation") @Suppress("Deprecation")

View File

@ -6,7 +6,6 @@ import org.junit.Assert.assertEquals
import org.junit.Test import org.junit.Test
class ReportableExceptionTest { class ReportableExceptionTest {
@Test @Test
fun bundle() { fun bundle() {
val reportableException = ReportableExceptionFixture.new() val reportableException = ReportableExceptionFixture.new()

View File

@ -9,7 +9,8 @@ import java.io.File
@Suppress("ReturnCount") @Suppress("ReturnCount")
suspend fun ExceptionPath.getExceptionDirectory(context: Context): File? { suspend fun ExceptionPath.getExceptionDirectory(context: Context): File? {
val exceptionDirectory = context.getExternalFilesDirSuspend(null) val exceptionDirectory =
context.getExternalFilesDirSuspend(null)
?.let { File(File(it, ExceptionPath.LOG_DIRECTORY_NAME), ExceptionPath.EXCEPTION_DIRECTORY_NAME) } ?.let { File(File(it, ExceptionPath.LOG_DIRECTORY_NAME), ExceptionPath.EXCEPTION_DIRECTORY_NAME) }
if (null == exceptionDirectory) { if (null == exceptionDirectory) {
@ -27,8 +28,12 @@ suspend fun ExceptionPath.getExceptionDirectory(context: Context): File? {
return exceptionDirectory return exceptionDirectory
} }
suspend fun ExceptionPath.getExceptionPath(context: Context, exception: ReportableException): File? { suspend fun ExceptionPath.getExceptionPath(
val exceptionDirectory = getExceptionDirectory(context) context: Context,
exception: ReportableException
): File? {
val exceptionDirectory =
getExceptionDirectory(context)
?: return null ?: return null
return File(exceptionDirectory, newExceptionFileName(exception)) return File(exceptionDirectory, newExceptionFileName(exception))

View File

@ -11,7 +11,6 @@ import co.electriccoin.zcash.spackle.process.ProcessNameCompat
import java.util.Collections import java.util.Collections
object GlobalCrashReporter { object GlobalCrashReporter {
internal const val CRASH_PROCESS_NAME_SUFFIX = ":crash" // $NON-NLS internal const val CRASH_PROCESS_NAME_SUFFIX = ":crash" // $NON-NLS
private val intrinsicLock = Any() private val intrinsicLock = Any()
@ -33,7 +32,8 @@ object GlobalCrashReporter {
synchronized(intrinsicLock) { synchronized(intrinsicLock) {
if (registeredCrashReporters == null) { if (registeredCrashReporters == null) {
registeredCrashReporters = Collections.synchronizedList( registeredCrashReporters =
Collections.synchronizedList(
// To prevent a race condition, register the LocalCrashReporter first. // To prevent a race condition, register the LocalCrashReporter first.
// FirebaseCrashReporter does some asynchronous registration internally, while // FirebaseCrashReporter does some asynchronous registration internally, while
// LocalCrashReporter uses AndroidUncaughtExceptionHandler which needs to read // LocalCrashReporter uses AndroidUncaughtExceptionHandler which needs to read

View File

@ -3,7 +3,6 @@ package co.electriccoin.zcash.crash.android.internal
import androidx.annotation.AnyThread import androidx.annotation.AnyThread
interface CrashReporter { interface CrashReporter {
/** /**
* Report a caught exception, e.g. within a try-catch. * Report a caught exception, e.g. within a try-catch.
*/ */

View File

@ -30,7 +30,8 @@ object FirebaseAppCache {
} }
} }
private suspend fun getFirebaseAppContainer(context: Context): FirebaseAppContainer = withContext(Dispatchers.IO) { private suspend fun getFirebaseAppContainer(context: Context): FirebaseAppContainer =
withContext(Dispatchers.IO) {
val firebaseApp = FirebaseApp.initializeApp(context) val firebaseApp = FirebaseApp.initializeApp(context)
FirebaseAppContainer(firebaseApp) FirebaseAppContainer(firebaseApp)
} }

View File

@ -25,11 +25,11 @@ import kotlinx.coroutines.async
internal class FirebaseCrashReporter( internal class FirebaseCrashReporter(
context: Context context: Context
) : CrashReporter { ) : CrashReporter {
@OptIn(kotlinx.coroutines.DelicateCoroutinesApi::class) @OptIn(kotlinx.coroutines.DelicateCoroutinesApi::class)
private val analyticsScope = CoroutineScope(SupervisorJob() + Dispatchers.Main) private val analyticsScope = CoroutineScope(SupervisorJob() + Dispatchers.Main)
private val initFirebaseJob: Deferred<CrashReporter?> = analyticsScope.async { private val initFirebaseJob: Deferred<CrashReporter?> =
analyticsScope.async {
FirebaseCrashReporterImpl.getInstance(context) FirebaseCrashReporterImpl.getInstance(context)
} }
@ -67,7 +67,6 @@ private class FirebaseCrashReporterImpl(
private val firebaseCrashlytics: FirebaseCrashlytics, private val firebaseCrashlytics: FirebaseCrashlytics,
private val firebaseInstallations: FirebaseInstallations private val firebaseInstallations: FirebaseInstallations
) : CrashReporter { ) : CrashReporter {
@AnyThread @AnyThread
override fun reportCaughtException(exception: Throwable) { override fun reportCaughtException(exception: Throwable) {
firebaseCrashlytics.recordException(exception) firebaseCrashlytics.recordException(exception)
@ -90,7 +89,8 @@ private class FirebaseCrashReporterImpl(
* early crashes may be missed. This is a tradeoff we are willing to make in order to avoid * early crashes may be missed. This is a tradeoff we are willing to make in order to avoid
* ANRs. * ANRs.
*/ */
private val lazyWithArgument = SuspendingLazy<Context, CrashReporter?> { private val lazyWithArgument =
SuspendingLazy<Context, CrashReporter?> {
if (it.resources.getBoolean(R.bool.co_electriccoin_zcash_crash_is_firebase_enabled)) { if (it.resources.getBoolean(R.bool.co_electriccoin_zcash_crash_is_firebase_enabled)) {
// Workaround for disk IO on main thread in Firebase initialization // Workaround for disk IO on main thread in Firebase initialization
@ -101,7 +101,8 @@ private class FirebaseCrashReporterImpl(
} }
val firebaseInstallations = FirebaseInstallations.getInstance(firebaseApp) val firebaseInstallations = FirebaseInstallations.getInstance(firebaseApp)
val firebaseCrashlytics = FirebaseCrashlytics.getInstance().apply { val firebaseCrashlytics =
FirebaseCrashlytics.getInstance().apply {
setCustomKey( setCustomKey(
CrashlyticsUserProperties.IS_TEST, CrashlyticsUserProperties.IS_TEST,
EmulatorWtfUtil.isEmulatorWtf(it) || FirebaseTestLabUtil.isFirebaseTestLab(it) EmulatorWtfUtil.isEmulatorWtf(it) || FirebaseTestLabUtil.isFirebaseTestLab(it)

View File

@ -8,8 +8,12 @@ import co.electriccoin.zcash.crash.android.getExceptionPath
import co.electriccoin.zcash.crash.write import co.electriccoin.zcash.crash.write
internal object AndroidExceptionReporter { internal object AndroidExceptionReporter {
internal suspend fun reportException(context: Context, reportableException: ReportableException) { internal suspend fun reportException(
val exceptionPath = ExceptionPath.getExceptionPath(context, reportableException) context: Context,
reportableException: ReportableException
) {
val exceptionPath =
ExceptionPath.getExceptionPath(context, reportableException)
?: return ?: return
reportableException.write(exceptionPath) reportableException.write(exceptionPath)

View File

@ -13,7 +13,8 @@ internal fun ReportableException.Companion.new(
isUncaught: Boolean, isUncaught: Boolean,
clock: Clock = Clock.System clock: Clock = Clock.System
): ReportableException { ): ReportableException {
val versionName = context.packageManager.getPackageInfoCompat(context.packageName, 0L).versionName val versionName =
context.packageManager.getPackageInfoCompat(context.packageName, 0L).versionName
?: "null" ?: "null"
return ReportableException( return ReportableException(
@ -25,7 +26,8 @@ internal fun ReportableException.Companion.new(
) )
} }
internal fun ReportableException.toBundle() = Bundle().apply { internal fun ReportableException.toBundle() =
Bundle().apply {
// Although Exception is Serializable, some Kotlin Coroutines exception classes break this // Although Exception is Serializable, some Kotlin Coroutines exception classes break this
// API contract. Therefore we have to convert to a string here. // API contract. Therefore we have to convert to a string here.
putSerializable(ReportableException.EXTRA_STRING_CLASS_NAME, exceptionClass) putSerializable(ReportableException.EXTRA_STRING_CLASS_NAME, exceptionClass)

View File

@ -12,13 +12,16 @@ internal class AndroidUncaughtExceptionHandler(
context: Context, context: Context,
private val defaultUncaughtExceptionHandler: Thread.UncaughtExceptionHandler private val defaultUncaughtExceptionHandler: Thread.UncaughtExceptionHandler
) : Thread.UncaughtExceptionHandler { ) : Thread.UncaughtExceptionHandler {
private val applicationContext = context.applicationContext private val applicationContext = context.applicationContext
override fun uncaughtException(t: Thread, e: Throwable) { override fun uncaughtException(
t: Thread,
e: Throwable
) {
val reportableException = ReportableException.new(applicationContext, e, true) val reportableException = ReportableException.new(applicationContext, e, true)
val isUseSecondaryProcess = applicationContext.resources val isUseSecondaryProcess =
applicationContext.resources
.getBoolean(R.bool.co_electriccoin_zcash_crash_is_use_secondary_process) .getBoolean(R.bool.co_electriccoin_zcash_crash_is_use_secondary_process)
if (isUseSecondaryProcess) { if (isUseSecondaryProcess) {
@ -31,7 +34,6 @@ internal class AndroidUncaughtExceptionHandler(
} }
companion object { companion object {
private val isInitialized = AtomicBoolean(false) private val isInitialized = AtomicBoolean(false)
/** /**

View File

@ -8,16 +8,18 @@ import kotlinx.coroutines.GlobalScope
@OptIn(kotlinx.coroutines.DelicateCoroutinesApi::class) @OptIn(kotlinx.coroutines.DelicateCoroutinesApi::class)
class ExceptionReceiver : CoroutineBroadcastReceiver(GlobalScope) { class ExceptionReceiver : CoroutineBroadcastReceiver(GlobalScope) {
override suspend fun onReceiveSuspend(
override suspend fun onReceiveSuspend(context: Context, intent: Intent) { context: Context,
val reportableException = intent.extras?.let { ReportableException.fromBundle(it) } intent: Intent
) {
val reportableException =
intent.extras?.let { ReportableException.fromBundle(it) }
?: return ?: return
AndroidExceptionReporter.reportException(context, reportableException) AndroidExceptionReporter.reportException(context, reportableException)
} }
companion object { companion object {
/** /**
* @return Explicit intent to broadcast to log the exception. * @return Explicit intent to broadcast to log the exception.
*/ */

View File

@ -14,7 +14,6 @@ import kotlinx.coroutines.launch
* Registers an exception handler to write exceptions to disk. * Registers an exception handler to write exceptions to disk.
*/ */
internal class LocalCrashReporter(private val applicationContext: Context) : CrashReporter { internal class LocalCrashReporter(private val applicationContext: Context) : CrashReporter {
private val crashReportingScope = CoroutineScope(SupervisorJob() + Dispatchers.Main) private val crashReportingScope = CoroutineScope(SupervisorJob() + Dispatchers.Main)
@AnyThread @AnyThread
@ -36,7 +35,8 @@ internal class LocalCrashReporter(private val applicationContext: Context) : Cra
} }
companion object { companion object {
private val lazyWithArgument = LazyWithArgument<Context, CrashReporter> { private val lazyWithArgument =
LazyWithArgument<Context, CrashReporter> {
AndroidUncaughtExceptionHandler.register(it) AndroidUncaughtExceptionHandler.register(it)
LocalCrashReporter(it.applicationContext) LocalCrashReporter(it.applicationContext)
} }

View File

@ -9,6 +9,5 @@ data class ReportableException(
val isUncaught: Boolean, val isUncaught: Boolean,
val time: Instant val time: Instant
) { ) {
companion object companion object
} }

View File

@ -14,8 +14,10 @@ object ExceptionPath {
const val TYPE = "txt" const val TYPE = "txt"
@Suppress("MaxLineLength") @Suppress("MaxLineLength")
fun newExceptionFileName(exception: ReportableException, uuid: UUID = UUID.randomUUID()) = fun newExceptionFileName(
"${exception.time.epochSeconds}$SEPARATOR$uuid$SEPARATOR${exception.exceptionClass}$SEPARATOR${exception.isUncaught}.$TYPE" exception: ReportableException,
uuid: UUID = UUID.randomUUID()
) = "${exception.time.epochSeconds}$SEPARATOR$uuid$SEPARATOR${exception.exceptionClass}$SEPARATOR${exception.isUncaught}.$TYPE"
// The exceptions are really just for debugging // The exceptions are really just for debugging
@Suppress("ThrowsCount") @Suppress("ThrowsCount")

View File

@ -1,4 +1,4 @@
@file:Suppress("ktlint:filename") @file:Suppress("ktlint:standard:filename")
package co.electriccoin.zcash.crash package co.electriccoin.zcash.crash
@ -8,7 +8,8 @@ import kotlinx.coroutines.withContext
import java.io.File import java.io.File
suspend fun ReportableException.write(path: File) { suspend fun ReportableException.write(path: File) {
val exceptionString = buildString { val exceptionString =
buildString {
appendLine("App version: $appVersion") appendLine("App version: $appVersion")
appendLine("Is uncaught: $isUncaught") appendLine("Is uncaught: $isUncaught")
appendLine("Time: $time") appendLine("Time: $time")

View File

@ -1,4 +1,4 @@
@file:Suppress("ktlint:filename") @file:Suppress("ktlint:standard:filename")
package co.electriccoin.zcash.crash package co.electriccoin.zcash.crash

View File

@ -143,7 +143,7 @@ FULLADLE_VERSION=0.17.4
GOOGLE_PLAY_SERVICES_GRADLE_PLUGIN_VERSION=4.3.15 GOOGLE_PLAY_SERVICES_GRADLE_PLUGIN_VERSION=4.3.15
GRADLE_VERSIONS_PLUGIN_VERSION=0.47.0 GRADLE_VERSIONS_PLUGIN_VERSION=0.47.0
JGIT_VERSION=6.4.0.202211300538-r JGIT_VERSION=6.4.0.202211300538-r
KTLINT_VERSION=0.49.0 KTLINT_VERSION=1.0.1
ACCOMPANIST_PERMISSIONS_VERSION=0.32.0 ACCOMPANIST_PERMISSIONS_VERSION=0.32.0
ANDROIDX_ACTIVITY_VERSION=1.8.1 ANDROIDX_ACTIVITY_VERSION=1.8.1

View File

@ -4,10 +4,12 @@ import co.electriccoin.zcash.preference.model.entry.PreferenceKey
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
interface PreferenceProvider { interface PreferenceProvider {
suspend fun hasKey(key: PreferenceKey): Boolean suspend fun hasKey(key: PreferenceKey): Boolean
suspend fun putString(key: PreferenceKey, value: String?) suspend fun putString(
key: PreferenceKey,
value: String?
)
suspend fun getString(key: PreferenceKey): String? suspend fun getString(key: PreferenceKey): String?

View File

@ -6,9 +6,9 @@ data class BooleanPreferenceDefault(
override val key: PreferenceKey, override val key: PreferenceKey,
private val defaultValue: Boolean private val defaultValue: Boolean
) : PreferenceDefault<Boolean> { ) : PreferenceDefault<Boolean> {
@Suppress("SwallowedException") @Suppress("SwallowedException")
override suspend fun getValue(preferenceProvider: PreferenceProvider) = preferenceProvider.getString(key)?.let { override suspend fun getValue(preferenceProvider: PreferenceProvider) =
preferenceProvider.getString(key)?.let {
try { try {
it.toBooleanStrict() it.toBooleanStrict()
} catch (e: IllegalArgumentException) { } catch (e: IllegalArgumentException) {
@ -18,7 +18,10 @@ data class BooleanPreferenceDefault(
} }
} ?: defaultValue } ?: defaultValue
override suspend fun putValue(preferenceProvider: PreferenceProvider, newValue: Boolean) { override suspend fun putValue(
preferenceProvider: PreferenceProvider,
newValue: Boolean
) {
preferenceProvider.putString(key, newValue.toString()) preferenceProvider.putString(key, newValue.toString())
} }
} }

View File

@ -6,8 +6,8 @@ data class IntegerPreferenceDefault(
override val key: PreferenceKey, override val key: PreferenceKey,
private val defaultValue: Int private val defaultValue: Int
) : PreferenceDefault<Int> { ) : PreferenceDefault<Int> {
override suspend fun getValue(preferenceProvider: PreferenceProvider) =
override suspend fun getValue(preferenceProvider: PreferenceProvider) = preferenceProvider.getString(key)?.let { preferenceProvider.getString(key)?.let {
try { try {
it.toInt() it.toInt()
} catch (e: NumberFormatException) { } catch (e: NumberFormatException) {
@ -17,7 +17,10 @@ data class IntegerPreferenceDefault(
} }
} ?: defaultValue } ?: defaultValue
override suspend fun putValue(preferenceProvider: PreferenceProvider, newValue: Int) { override suspend fun putValue(
preferenceProvider: PreferenceProvider,
newValue: Int
) {
preferenceProvider.putString(key, newValue.toString()) preferenceProvider.putString(key, newValue.toString())
} }
} }

View File

@ -11,6 +11,7 @@ import kotlinx.coroutines.flow.map
* variation in default value. Clients define the key and default value together, rather than just * variation in default value. Clients define the key and default value together, rather than just
* the key. * the key.
*/ */
interface PreferenceDefault<T> {
/* /*
* API note: the default value is not available through the public interface in order to prevent * API note: the default value is not available through the public interface in order to prevent
* clients from accidentally using the default value instead of the preference value. * clients from accidentally using the default value instead of the preference value.
@ -20,7 +21,6 @@ import kotlinx.coroutines.flow.map
* Overall the number of Integer preference entries is expected to be low compared to Booleans, * Overall the number of Integer preference entries is expected to be low compared to Booleans,
* and perhaps many Integer values will also fit within the autoboxing cache. * and perhaps many Integer values will also fit within the autoboxing cache.
*/ */
interface PreferenceDefault<T> {
val key: PreferenceKey val key: PreferenceKey
@ -34,14 +34,18 @@ interface PreferenceDefault<T> {
* @param preferenceProvider Provides actual preference values. * @param preferenceProvider Provides actual preference values.
* @param newValue New value to write. * @param newValue New value to write.
*/ */
suspend fun putValue(preferenceProvider: PreferenceProvider, newValue: T) suspend fun putValue(
preferenceProvider: PreferenceProvider,
newValue: T
)
/** /**
* @param preferenceProvider Provides actual preference values. * @param preferenceProvider Provides actual preference values.
* @return Flow that emits preference changes. Note that implementations should emit an initial value * @return Flow that emits preference changes. Note that implementations should emit an initial value
* indicating what was stored in the preferences, in addition to subsequent updates. * indicating what was stored in the preferences, in addition to subsequent updates.
*/ */
fun observe(preferenceProvider: PreferenceProvider): Flow<T> = preferenceProvider.observe(key) fun observe(preferenceProvider: PreferenceProvider): Flow<T> =
preferenceProvider.observe(key)
.map { getValue(preferenceProvider) } .map { getValue(preferenceProvider) }
.distinctUntilChanged() .distinctUntilChanged()
} }

View File

@ -6,11 +6,14 @@ data class StringPreferenceDefault(
override val key: PreferenceKey, override val key: PreferenceKey,
private val defaultValue: String private val defaultValue: String
) : PreferenceDefault<String> { ) : PreferenceDefault<String> {
override suspend fun getValue(preferenceProvider: PreferenceProvider) =
override suspend fun getValue(preferenceProvider: PreferenceProvider) = preferenceProvider.getString(key) preferenceProvider.getString(key)
?: defaultValue ?: defaultValue
override suspend fun putValue(preferenceProvider: PreferenceProvider, newValue: String) { override suspend fun putValue(
preferenceProvider: PreferenceProvider,
newValue: String
) {
preferenceProvider.putString(key, newValue) preferenceProvider.putString(key, newValue)
} }
} }

View File

@ -16,30 +16,36 @@ class BooleanPreferenceDefaultTest {
} }
@Test @Test
fun value_default_true() = runTest { fun value_default_true() =
runTest {
val entry = BooleanPreferenceDefaultFixture.newTrue() val entry = BooleanPreferenceDefaultFixture.newTrue()
assertTrue(entry.getValue(MockPreferenceProvider())) assertTrue(entry.getValue(MockPreferenceProvider()))
} }
@Test @Test
fun value_default_false() = runTest { fun value_default_false() =
runTest {
val entry = BooleanPreferenceDefaultFixture.newFalse() val entry = BooleanPreferenceDefaultFixture.newFalse()
assertFalse(entry.getValue(MockPreferenceProvider())) assertFalse(entry.getValue(MockPreferenceProvider()))
} }
@Test @Test
fun value_from_config_false() = runTest { fun value_from_config_false() =
runTest {
val entry = BooleanPreferenceDefaultFixture.newTrue() val entry = BooleanPreferenceDefaultFixture.newTrue()
val mockPreferenceProvider = MockPreferenceProvider { val mockPreferenceProvider =
MockPreferenceProvider {
mutableMapOf(BooleanPreferenceDefaultFixture.KEY.key to false.toString()) mutableMapOf(BooleanPreferenceDefaultFixture.KEY.key to false.toString())
} }
assertFalse(entry.getValue(mockPreferenceProvider)) assertFalse(entry.getValue(mockPreferenceProvider))
} }
@Test @Test
fun value_from_config_true() = runTest { fun value_from_config_true() =
runTest {
val entry = BooleanPreferenceDefaultFixture.newTrue() val entry = BooleanPreferenceDefaultFixture.newTrue()
val mockPreferenceProvider = MockPreferenceProvider { val mockPreferenceProvider =
MockPreferenceProvider {
mutableMapOf(BooleanPreferenceDefaultFixture.KEY.key to true.toString()) mutableMapOf(BooleanPreferenceDefaultFixture.KEY.key to true.toString())
} }
assertTrue(entry.getValue(mockPreferenceProvider)) assertTrue(entry.getValue(mockPreferenceProvider))

View File

@ -15,17 +15,20 @@ class IntegerPreferenceDefaultTest {
} }
@Test @Test
fun value_default() = runTest { fun value_default() =
runTest {
val entry = IntegerPreferenceDefaultFixture.new() val entry = IntegerPreferenceDefaultFixture.new()
assertEquals(IntegerPreferenceDefaultFixture.DEFAULT_VALUE, entry.getValue(MockPreferenceProvider())) assertEquals(IntegerPreferenceDefaultFixture.DEFAULT_VALUE, entry.getValue(MockPreferenceProvider()))
} }
@Test @Test
fun value_override() = runTest { fun value_override() =
runTest {
val expected = IntegerPreferenceDefaultFixture.DEFAULT_VALUE + 5 val expected = IntegerPreferenceDefaultFixture.DEFAULT_VALUE + 5
val entry = IntegerPreferenceDefaultFixture.new() val entry = IntegerPreferenceDefaultFixture.new()
val mockPreferenceProvider = MockPreferenceProvider { val mockPreferenceProvider =
MockPreferenceProvider {
mutableMapOf(StringDefaultPreferenceFixture.KEY.key to expected.toString()) mutableMapOf(StringDefaultPreferenceFixture.KEY.key to expected.toString())
} }

View File

@ -14,16 +14,19 @@ class StringPreferenceDefaultTest {
} }
@Test @Test
fun value_default() = runTest { fun value_default() =
runTest {
val entry = StringDefaultPreferenceFixture.new() val entry = StringDefaultPreferenceFixture.new()
assertEquals(StringDefaultPreferenceFixture.DEFAULT_VALUE, entry.getValue(MockPreferenceProvider())) assertEquals(StringDefaultPreferenceFixture.DEFAULT_VALUE, entry.getValue(MockPreferenceProvider()))
} }
@Test @Test
fun value_override() = runTest { fun value_override() =
runTest {
val entry = StringDefaultPreferenceFixture.new() val entry = StringDefaultPreferenceFixture.new()
val mockPreferenceProvider = MockPreferenceProvider { val mockPreferenceProvider =
MockPreferenceProvider {
mutableMapOf(StringDefaultPreferenceFixture.KEY.key to "override") mutableMapOf(StringDefaultPreferenceFixture.KEY.key to "override")
} }

View File

@ -11,7 +11,6 @@ import kotlinx.coroutines.flow.flow
class MockPreferenceProvider( class MockPreferenceProvider(
mutableMapFactory: () -> MutableMap<String, String?> = { mutableMapOf() } mutableMapFactory: () -> MutableMap<String, String?> = { mutableMapOf() }
) : PreferenceProvider { ) : PreferenceProvider {
private val map = mutableMapFactory() private val map = mutableMapFactory()
override suspend fun getString(key: PreferenceKey) = map[key.key] override suspend fun getString(key: PreferenceKey) = map[key.key]
@ -21,7 +20,10 @@ class MockPreferenceProvider(
override suspend fun hasKey(key: PreferenceKey) = map.containsKey(key.key) override suspend fun hasKey(key: PreferenceKey) = map.containsKey(key.key)
override suspend fun putString(key: PreferenceKey, value: String?) { override suspend fun putString(
key: PreferenceKey,
value: String?
) {
map[key.key] = value map[key.key] = value
} }
} }

View File

@ -5,6 +5,8 @@ import co.electriccoin.zcash.preference.model.entry.PreferenceKey
object BooleanPreferenceDefaultFixture { object BooleanPreferenceDefaultFixture {
val KEY = PreferenceKey("some_boolean_key") // $NON-NLS val KEY = PreferenceKey("some_boolean_key") // $NON-NLS
fun newTrue() = BooleanPreferenceDefault(KEY, true) fun newTrue() = BooleanPreferenceDefault(KEY, true)
fun newFalse() = BooleanPreferenceDefault(KEY, false) fun newFalse() = BooleanPreferenceDefault(KEY, false)
} }

View File

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

View File

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

View File

@ -35,10 +35,12 @@ class EncryptedPreferenceProviderTest {
@Test @Test
@SmallTest @SmallTest
fun put_and_get_string() = runBlocking { fun put_and_get_string() =
runBlocking {
val expectedValue = StringDefaultPreferenceFixture.DEFAULT_VALUE + "extra" val expectedValue = StringDefaultPreferenceFixture.DEFAULT_VALUE + "extra"
val preferenceProvider = new().apply { val preferenceProvider =
new().apply {
putString(StringDefaultPreferenceFixture.KEY, expectedValue) putString(StringDefaultPreferenceFixture.KEY, expectedValue)
} }
@ -47,7 +49,8 @@ class EncryptedPreferenceProviderTest {
@Test @Test
@SmallTest @SmallTest
fun hasKey_false() = runBlocking { fun hasKey_false() =
runBlocking {
val preferenceProvider = new() val preferenceProvider = new()
assertFalse(preferenceProvider.hasKey(StringDefaultPreferenceFixture.new().key)) assertFalse(preferenceProvider.hasKey(StringDefaultPreferenceFixture.new().key))
@ -55,10 +58,12 @@ class EncryptedPreferenceProviderTest {
@Test @Test
@SmallTest @SmallTest
fun put_and_check_key() = runBlocking { fun put_and_check_key() =
runBlocking {
val expectedValue = StringDefaultPreferenceFixture.DEFAULT_VALUE + "extra" val expectedValue = StringDefaultPreferenceFixture.DEFAULT_VALUE + "extra"
val preferenceProvider = new().apply { val preferenceProvider =
new().apply {
putString(StringDefaultPreferenceFixture.KEY, expectedValue) putString(StringDefaultPreferenceFixture.KEY, expectedValue)
} }
@ -69,14 +74,16 @@ class EncryptedPreferenceProviderTest {
// e.g. the directory path and the fact the preferences are stored as XML // e.g. the directory path and the fact the preferences are stored as XML
@Test @Test
@SmallTest @SmallTest
fun verify_no_plaintext() = runBlocking { fun verify_no_plaintext() =
runBlocking {
val expectedValue = StringDefaultPreferenceFixture.DEFAULT_VALUE + "extra" val expectedValue = StringDefaultPreferenceFixture.DEFAULT_VALUE + "extra"
new().apply { new().apply {
putString(StringDefaultPreferenceFixture.KEY, expectedValue) putString(StringDefaultPreferenceFixture.KEY, expectedValue)
} }
val text = File( val text =
File(
File(ApplicationProvider.getApplicationContext<Context>().dataDir, "shared_prefs"), File(ApplicationProvider.getApplicationContext<Context>().dataDir, "shared_prefs"),
"$FILENAME.xml" "$FILENAME.xml"
).readText() ).readText()
@ -87,7 +94,9 @@ class EncryptedPreferenceProviderTest {
companion object { companion object {
private val FILENAME = "encrypted_preference_test" private val FILENAME = "encrypted_preference_test"
private suspend fun new() = AndroidPreferenceProvider.newEncrypted(
private suspend fun new() =
AndroidPreferenceProvider.newEncrypted(
ApplicationProvider.getApplicationContext(), ApplicationProvider.getApplicationContext(),
FILENAME FILENAME
) )

View File

@ -33,10 +33,12 @@ class StandardPreferenceProviderTest {
@Test @Test
@SmallTest @SmallTest
fun put_and_get_string() = runBlocking { fun put_and_get_string() =
runBlocking {
val expectedValue = StringDefaultPreferenceFixture.DEFAULT_VALUE + "extra" val expectedValue = StringDefaultPreferenceFixture.DEFAULT_VALUE + "extra"
val preferenceProvider = new().apply { val preferenceProvider =
new().apply {
putString(StringDefaultPreferenceFixture.KEY, expectedValue) putString(StringDefaultPreferenceFixture.KEY, expectedValue)
} }
@ -45,7 +47,8 @@ class StandardPreferenceProviderTest {
@Test @Test
@SmallTest @SmallTest
fun hasKey_false() = runBlocking { fun hasKey_false() =
runBlocking {
val preferenceProvider = new() val preferenceProvider = new()
assertFalse(preferenceProvider.hasKey(StringDefaultPreferenceFixture.new().key)) assertFalse(preferenceProvider.hasKey(StringDefaultPreferenceFixture.new().key))
@ -53,10 +56,12 @@ class StandardPreferenceProviderTest {
@Test @Test
@SmallTest @SmallTest
fun put_and_check_key() = runBlocking { fun put_and_check_key() =
runBlocking {
val expectedValue = StringDefaultPreferenceFixture.DEFAULT_VALUE + "extra" val expectedValue = StringDefaultPreferenceFixture.DEFAULT_VALUE + "extra"
val preferenceProvider = new().apply { val preferenceProvider =
new().apply {
putString(StringDefaultPreferenceFixture.KEY, expectedValue) putString(StringDefaultPreferenceFixture.KEY, expectedValue)
} }
@ -65,7 +70,9 @@ class StandardPreferenceProviderTest {
companion object { companion object {
private val FILENAME = "encrypted_preference_test" private val FILENAME = "encrypted_preference_test"
private suspend fun new() = AndroidPreferenceProvider.newStandard(
private suspend fun new() =
AndroidPreferenceProvider.newStandard(
ApplicationProvider.getApplicationContext(), ApplicationProvider.getApplicationContext(),
FILENAME FILENAME
) )

View File

@ -6,6 +6,7 @@ import co.electriccoin.zcash.preference.model.entry.StringPreferenceDefault
object StringDefaultPreferenceFixture { object StringDefaultPreferenceFixture {
val KEY = PreferenceKey("some_string_key") // $NON-NLS val KEY = PreferenceKey("some_string_key") // $NON-NLS
const val DEFAULT_VALUE = "some_default_value" // $NON-NLS const val DEFAULT_VALUE = "some_default_value" // $NON-NLS
fun new( fun new(
preferenceKey: PreferenceKey = KEY, preferenceKey: PreferenceKey = KEY,
value: String = DEFAULT_VALUE value: String = DEFAULT_VALUE

View File

@ -26,21 +26,25 @@ import java.util.concurrent.Executors
* this instance lives for the lifetime of the application. Constructing multiple instances will * this instance lives for the lifetime of the application. Constructing multiple instances will
* potentially corrupt preference data and will leak resources. * potentially corrupt preference data and will leak resources.
*/ */
/*
* Implementation note: EncryptedSharedPreferences are not thread-safe, so this implementation
* confines them to a single background thread.
*/
class AndroidPreferenceProvider( class AndroidPreferenceProvider(
private val sharedPreferences: SharedPreferences, private val sharedPreferences: SharedPreferences,
private val dispatcher: CoroutineDispatcher private val dispatcher: CoroutineDispatcher
) : PreferenceProvider { ) : PreferenceProvider {
/*
* Implementation note: EncryptedSharedPreferences are not thread-safe, so this implementation
* confines them to a single background thread.
*/
override suspend fun hasKey(key: PreferenceKey) = withContext(dispatcher) { override suspend fun hasKey(key: PreferenceKey) =
withContext(dispatcher) {
sharedPreferences.contains(key.key) sharedPreferences.contains(key.key)
} }
@SuppressLint("ApplySharedPref") @SuppressLint("ApplySharedPref")
override suspend fun putString(key: PreferenceKey, value: String?) = withContext(dispatcher) { override suspend fun putString(
key: PreferenceKey,
value: String?
) = withContext(dispatcher) {
val editor = sharedPreferences.edit() val editor = sharedPreferences.edit()
editor.putString(key.key, value) editor.putString(key.key, value)
@ -50,12 +54,15 @@ class AndroidPreferenceProvider(
Unit Unit
} }
override suspend fun getString(key: PreferenceKey) = withContext(dispatcher) { override suspend fun getString(key: PreferenceKey) =
withContext(dispatcher) {
sharedPreferences.getString(key.key, null) sharedPreferences.getString(key.key, null)
} }
override fun observe(key: PreferenceKey): Flow<String?> = callbackFlow<Unit> { override fun observe(key: PreferenceKey): Flow<String?> =
val listener = SharedPreferences.OnSharedPreferenceChangeListener { _, _ -> callbackFlow<Unit> {
val listener =
SharedPreferences.OnSharedPreferenceChangeListener { _, _ ->
// Callback on main thread // Callback on main thread
trySend(Unit) trySend(Unit)
} }
@ -71,35 +78,44 @@ class AndroidPreferenceProvider(
.map { getString(key) } .map { getString(key) }
companion object { companion object {
suspend fun newStandard(context: Context, filename: String): PreferenceProvider { suspend fun newStandard(
context: Context,
filename: String
): PreferenceProvider {
/* /*
* Because of this line, we don't want multiple instances of this object created * Because of this line, we don't want multiple instances of this object created
* because we don't clean up the thread afterwards. * because we don't clean up the thread afterwards.
*/ */
val singleThreadedDispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher() val singleThreadedDispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher()
val sharedPreferences = withContext(singleThreadedDispatcher) { val sharedPreferences =
withContext(singleThreadedDispatcher) {
context.getSharedPreferences(filename, Context.MODE_PRIVATE) context.getSharedPreferences(filename, Context.MODE_PRIVATE)
} }
return AndroidPreferenceProvider(sharedPreferences, singleThreadedDispatcher) return AndroidPreferenceProvider(sharedPreferences, singleThreadedDispatcher)
} }
suspend fun newEncrypted(context: Context, filename: String): PreferenceProvider { suspend fun newEncrypted(
context: Context,
filename: String
): PreferenceProvider {
/* /*
* Because of this line, we don't want multiple instances of this object created * Because of this line, we don't want multiple instances of this object created
* because we don't clean up the thread afterwards. * because we don't clean up the thread afterwards.
*/ */
val singleThreadedDispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher() val singleThreadedDispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher()
val mainKey = withContext(singleThreadedDispatcher) { val mainKey =
withContext(singleThreadedDispatcher) {
@Suppress("BlockingMethodInNonBlockingContext") @Suppress("BlockingMethodInNonBlockingContext")
MasterKey.Builder(context).apply { MasterKey.Builder(context).apply {
setKeyScheme(MasterKey.KeyScheme.AES256_GCM) setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
}.build() }.build()
} }
val sharedPreferences = withContext(singleThreadedDispatcher) { val sharedPreferences =
withContext(singleThreadedDispatcher) {
@Suppress("BlockingMethodInNonBlockingContext") @Suppress("BlockingMethodInNonBlockingContext")
EncryptedSharedPreferences.create( EncryptedSharedPreferences.create(
context, context,

View File

@ -8,10 +8,10 @@ import org.junit.Test
import kotlin.test.assertEquals import kotlin.test.assertEquals
class PercentDecimalExtTest { class PercentDecimalExtTest {
@Test @Test
@SmallTest @SmallTest
fun parse_non_zero_percent_decimal_test() = runTest { fun parse_non_zero_percent_decimal_test() =
runTest {
val parsed = PercentDecimal(0.1234f).toPercentageWithDecimal() val parsed = PercentDecimal(0.1234f).toPercentageWithDecimal()
assertEquals("12${MonetarySeparators.current().decimal}34", parsed) assertEquals("12${MonetarySeparators.current().decimal}34", parsed)
@ -19,7 +19,8 @@ class PercentDecimalExtTest {
@Test @Test
@SmallTest @SmallTest
fun parse_zero_percent_decimal_test() = runTest { fun parse_zero_percent_decimal_test() =
runTest {
val parsed = PercentDecimal(0.0000f).toPercentageWithDecimal() val parsed = PercentDecimal(0.0000f).toPercentageWithDecimal()
assertEquals("0${MonetarySeparators.current().decimal}00", parsed) assertEquals("0${MonetarySeparators.current().decimal}00", parsed)
@ -27,7 +28,8 @@ class PercentDecimalExtTest {
@Test @Test
@SmallTest @SmallTest
fun parse_max_percent_decimal_test() = runTest { fun parse_max_percent_decimal_test() =
runTest {
val parsed = PercentDecimal(1f).toPercentageWithDecimal() val parsed = PercentDecimal(1f).toPercentageWithDecimal()
assertEquals("100${MonetarySeparators.current().decimal}00", parsed) assertEquals("100${MonetarySeparators.current().decimal}00", parsed)
@ -35,7 +37,8 @@ class PercentDecimalExtTest {
@Test @Test
@SmallTest @SmallTest
fun parse_min_percent_decimal_test() = runTest { fun parse_min_percent_decimal_test() =
runTest {
val parsed = PercentDecimal(0f).toPercentageWithDecimal() val parsed = PercentDecimal(0f).toPercentageWithDecimal()
assertEquals("0${MonetarySeparators.current().decimal}00", parsed) assertEquals("0${MonetarySeparators.current().decimal}00", parsed)
@ -43,7 +46,8 @@ class PercentDecimalExtTest {
@Test @Test
@SmallTest @SmallTest
fun parse_round_down_percent_decimal_test() = runTest { fun parse_round_down_percent_decimal_test() =
runTest {
val parsed = PercentDecimal(0.11111f).toPercentageWithDecimal() val parsed = PercentDecimal(0.11111f).toPercentageWithDecimal()
assertEquals("11${MonetarySeparators.current().decimal}11", parsed) assertEquals("11${MonetarySeparators.current().decimal}11", parsed)
@ -51,7 +55,8 @@ class PercentDecimalExtTest {
@Test @Test
@SmallTest @SmallTest
fun parse_round_up_percent_decimal_test() = runTest { fun parse_round_up_percent_decimal_test() =
runTest {
val parsed = PercentDecimal(0.11119f).toPercentageWithDecimal() val parsed = PercentDecimal(0.11119f).toPercentageWithDecimal()
assertEquals("11${MonetarySeparators.current().decimal}12", parsed) assertEquals("11${MonetarySeparators.current().decimal}12", parsed)

View File

@ -15,7 +15,6 @@ import kotlin.test.assertNotNull
import kotlin.test.assertTrue import kotlin.test.assertTrue
class ZecRequestTest { class ZecRequestTest {
companion object { companion object {
private const val URI: String = "zcash:tmXuTnE11JojToagTqxXUn6KvdxDE3iLKbp?amount=1&message=Hello%20world!" private const val URI: String = "zcash:tmXuTnE11JojToagTqxXUn6KvdxDE3iLKbp?amount=1&message=Hello%20world!"
@ -23,7 +22,8 @@ class ZecRequestTest {
private val AMOUNT = Zatoshi(1) private val AMOUNT = Zatoshi(1)
private val MESSAGE = ZecRequestMessage("Hello world!") private val MESSAGE = ZecRequestMessage("Hello world!")
private const val ADDRESS_STRING = "tmXuTnE11JojToagTqxXUn6KvdxDE3iLKbp" private const val ADDRESS_STRING = "tmXuTnE11JojToagTqxXUn6KvdxDE3iLKbp"
private val ADDRESS: WalletAddress.Unified = runBlocking { private val ADDRESS: WalletAddress.Unified =
runBlocking {
WalletAddress.Unified.new(ADDRESS_STRING) WalletAddress.Unified.new(ADDRESS_STRING)
} }
val REQUEST = ZecRequest(ADDRESS, AMOUNT, MESSAGE) val REQUEST = ZecRequest(ADDRESS, AMOUNT, MESSAGE)
@ -31,7 +31,8 @@ class ZecRequestTest {
@Test @Test
@SmallTest @SmallTest
fun parse_uri_not_null() = runTest { fun parse_uri_not_null() =
runTest {
val parsed = ZecRequest.fromUri(Zip321UriParseFixture.URI) val parsed = ZecRequest.fromUri(Zip321UriParseFixture.URI)
assertNotNull(parsed) assertNotNull(parsed)
@ -39,7 +40,8 @@ class ZecRequestTest {
@Test @Test
@SmallTest @SmallTest
fun parse_uri_valid_result() = runTest { fun parse_uri_valid_result() =
runTest {
val parsed = ZecRequest.fromUri(Zip321UriParseFixture.URI) val parsed = ZecRequest.fromUri(Zip321UriParseFixture.URI)
assertTrue(parsed.message.value.length <= ZecRequestMessage.MAX_MESSAGE_LENGTH) assertTrue(parsed.message.value.length <= ZecRequestMessage.MAX_MESSAGE_LENGTH)
@ -49,9 +51,11 @@ class ZecRequestTest {
@Test @Test
@SmallTest @SmallTest
fun parse_uri_correct_result() = runTest { fun parse_uri_correct_result() =
runTest {
val parsed = ZecRequest.fromUri(Zip321UriParseFixture.URI) val parsed = ZecRequest.fromUri(Zip321UriParseFixture.URI)
val expected = ZecRequest( val expected =
ZecRequest(
WalletAddress.Unified.new(Zip321UriParseFixture.ADDRESS), WalletAddress.Unified.new(Zip321UriParseFixture.ADDRESS),
Zip321UriParseFixture.AMOUNT, Zip321UriParseFixture.AMOUNT,
Zip321UriParseFixture.MESSAGE Zip321UriParseFixture.MESSAGE
@ -64,10 +68,12 @@ class ZecRequestTest {
@SmallTest @SmallTest
// TODO [#397]: Waiting for an implementation of Uri parser in SDK project // TODO [#397]: Waiting for an implementation of Uri parser in SDK project
@Ignore("Waiting for an implementation of Uri parser in SDK project") @Ignore("Waiting for an implementation of Uri parser in SDK project")
fun parse_uri_incorrect_result() = runTest { fun parse_uri_incorrect_result() =
runTest {
val parsed = ZecRequest.fromUri(URI) val parsed = ZecRequest.fromUri(URI)
val expected = REQUEST val expected = REQUEST
val actual = ZecRequest( val actual =
ZecRequest(
WalletAddress.Unified.new(Zip321UriParseFixture.ADDRESS), WalletAddress.Unified.new(Zip321UriParseFixture.ADDRESS),
Zip321UriParseFixture.AMOUNT, Zip321UriParseFixture.AMOUNT,
Zip321UriParseFixture.MESSAGE Zip321UriParseFixture.MESSAGE
@ -79,7 +85,8 @@ class ZecRequestTest {
@Test @Test
@SmallTest @SmallTest
fun build_uri_not_null() = runTest { fun build_uri_not_null() =
runTest {
val request = Zip321UriBuildFixture.REQUEST val request = Zip321UriBuildFixture.REQUEST
val built = request.toUri() val built = request.toUri()
@ -88,7 +95,8 @@ class ZecRequestTest {
@Test @Test
@SmallTest @SmallTest
fun build_uri_valid_result() = runTest { fun build_uri_valid_result() =
runTest {
val request = Zip321UriBuildFixture.REQUEST val request = Zip321UriBuildFixture.REQUEST
val built = request.toUri() val built = request.toUri()
@ -98,7 +106,8 @@ class ZecRequestTest {
@Test @Test
@SmallTest @SmallTest
fun built_uri_correct_result() = runTest { fun built_uri_correct_result() =
runTest {
val request = Zip321UriBuildFixture.REQUEST val request = Zip321UriBuildFixture.REQUEST
val built = request.toUri() val built = request.toUri()
val expected = Zip321UriBuildFixture.URI val expected = Zip321UriBuildFixture.URI
@ -110,7 +119,8 @@ class ZecRequestTest {
@SmallTest @SmallTest
// TODO [#397]: Waiting for an implementation of Uri parser in SDK project // TODO [#397]: Waiting for an implementation of Uri parser in SDK project
@Ignore("Waiting for an implementation of Uri parser in SDK project") @Ignore("Waiting for an implementation of Uri parser in SDK project")
fun build_uri_incorrect_result() = runTest { fun build_uri_incorrect_result() =
runTest {
val request = Zip321UriBuildFixture.REQUEST val request = Zip321UriBuildFixture.REQUEST
val built = request.toUri() val built = request.toUri()
val expected = URI val expected = URI

View File

@ -7,7 +7,6 @@ import org.junit.Test
import kotlin.test.assertNotSame import kotlin.test.assertNotSame
class ZcashCurrencyTest { class ZcashCurrencyTest {
@SmallTest @SmallTest
@Test @Test
fun check_is_zec_type() { fun check_is_zec_type() {

View File

@ -1,4 +1,4 @@
@file:Suppress("ktlint:filename") @file:Suppress("ktlint:standard:filename")
package cash.z.ecc.sdk.extension package cash.z.ecc.sdk.extension
@ -12,7 +12,8 @@ fun PercentDecimal.toPercentageWithDecimal(decimalFormat: DecimalFormat = prepar
return decimalFormat.format(decimal * 100) return decimalFormat.format(decimal * 100)
} }
private fun preparePercentDecimalFormat(): DecimalFormat = DecimalFormat().apply { private fun preparePercentDecimalFormat(): DecimalFormat =
DecimalFormat().apply {
val monetarySeparators = MonetarySeparators.current() val monetarySeparators = MonetarySeparators.current()
val localizedPattern = "##0${monetarySeparators.decimal}00" val localizedPattern = "##0${monetarySeparators.decimal}00"
applyLocalizedPattern(localizedPattern) applyLocalizedPattern(localizedPattern)

View File

@ -1,4 +1,4 @@
@file:Suppress("ktlint:filename") @file:Suppress("ktlint:standard:filename")
package cash.z.ecc.sdk.extension package cash.z.ecc.sdk.extension
@ -6,7 +6,10 @@ import cash.z.ecc.android.sdk.Synchronizer
import cash.z.ecc.android.sdk.model.UnifiedSpendingKey import cash.z.ecc.android.sdk.model.UnifiedSpendingKey
import cash.z.ecc.android.sdk.model.ZecSend import cash.z.ecc.android.sdk.model.ZecSend
suspend fun Synchronizer.send(spendingKey: UnifiedSpendingKey, send: ZecSend) = sendToAddress( suspend fun Synchronizer.send(
spendingKey: UnifiedSpendingKey,
send: ZecSend
) = sendToAddress(
spendingKey, spendingKey,
send.amount, send.amount,
send.destination.address, send.destination.address,

View File

@ -9,7 +9,6 @@ import cash.z.ecc.android.sdk.model.ZcashNetwork
import co.electriccoin.lightwallet.client.model.LightWalletEndpoint import co.electriccoin.lightwallet.client.model.LightWalletEndpoint
object PersistableWalletFixture { object PersistableWalletFixture {
val NETWORK = ZcashNetwork.Mainnet val NETWORK = ZcashNetwork.Mainnet
val ENDPOINT = LightWalletEndpoint.Mainnet val ENDPOINT = LightWalletEndpoint.Mainnet

View File

@ -3,8 +3,9 @@ package cash.z.ecc.sdk.fixture
import cash.z.ecc.android.sdk.model.SeedPhrase import cash.z.ecc.android.sdk.model.SeedPhrase
object SeedPhraseFixture { object SeedPhraseFixture {
@Suppress("MaxLineLength") const val SEED_PHRASE =
val SEED_PHRASE = "still champion voice habit trend flight survey between bitter process artefact blind carbon truly provide dizzy crush flush breeze blouse charge solid fish spread" "still champion voice habit trend flight survey between bitter process artefact blind carbon " +
"truly provide dizzy crush flush breeze blouse charge solid fish spread"
fun new(seedPhrase: String = SEED_PHRASE) = SeedPhrase.new(seedPhrase) fun new(seedPhrase: String = SEED_PHRASE) = SeedPhrase.new(seedPhrase)
} }

View File

@ -9,13 +9,15 @@ import kotlinx.coroutines.runBlocking
object Zip321UriBuildFixture { object Zip321UriBuildFixture {
// TODO [#161]: Pending SDK support // TODO [#161]: Pending SDK support
const val URI: String = "zcash:Unified%20GitHub%20Issue%20#161?amount=123&message=Thank%20you%20" + const val URI: String =
"zcash:Unified%20GitHub%20Issue%20#161?amount=123&message=Thank%20you%20" +
"for%20your%20purchase" "for%20your%20purchase"
@Suppress("MagicNumber") @Suppress("MagicNumber")
val AMOUNT = Zatoshi(123) val AMOUNT = Zatoshi(123)
val MESSAGE = ZecRequestMessage("Thank you for your purchase") val MESSAGE = ZecRequestMessage("Thank you for your purchase")
val ADDRESS: WalletAddress.Unified = runBlocking { val ADDRESS: WalletAddress.Unified =
runBlocking {
WalletAddress.Unified.new(WalletAddressFixture.UNIFIED_ADDRESS_STRING) WalletAddress.Unified.new(WalletAddressFixture.UNIFIED_ADDRESS_STRING)
} }
val REQUEST = ZecRequest(ADDRESS, AMOUNT, MESSAGE) val REQUEST = ZecRequest(ADDRESS, AMOUNT, MESSAGE)

View File

@ -8,7 +8,8 @@ import cash.z.ecc.sdk.model.ZecRequestMessage
object Zip321UriParseFixture { object Zip321UriParseFixture {
// TODO [#161]: Pending SDK support // TODO [#161]: Pending SDK support
const val URI: String = "zcash:Unified%20GitHub%20Issue%20#161?amount=123&message=Thank%20you%20" + const val URI: String =
"zcash:Unified%20GitHub%20Issue%20#161?amount=123&message=Thank%20you%20" +
"for%20your%20purchase" "for%20your%20purchase"
const val ADDRESS: String = WalletAddressFixture.UNIFIED_ADDRESS_STRING const val ADDRESS: String = WalletAddressFixture.UNIFIED_ADDRESS_STRING
@ -20,7 +21,5 @@ object Zip321UriParseFixture {
// TODO [#397]: Waiting for an implementation of Uri parser in SDK project // TODO [#397]: Waiting for an implementation of Uri parser in SDK project
// Should return ZecRequest.fromUri(toParse) ideally, but it'd end up with an infinite loop for now. // Should return ZecRequest.fromUri(toParse) ideally, but it'd end up with an infinite loop for now.
@Suppress("UNUSED_PARAMETER") @Suppress("UNUSED_PARAMETER")
suspend fun new( suspend fun new(toParse: String = URI) = ZecRequest(WalletAddress.Unified.new(ADDRESS), AMOUNT, MESSAGE)
toParse: String = URI
) = ZecRequest(WalletAddress.Unified.new(ADDRESS), AMOUNT, MESSAGE)
} }

View File

@ -10,8 +10,11 @@ import java.util.Locale
// there as part of creating the object // there as part of creating the object
sealed class SeedPhraseValidation { sealed class SeedPhraseValidation {
object BadCount : SeedPhraseValidation() object BadCount : SeedPhraseValidation()
object BadWord : SeedPhraseValidation() object BadWord : SeedPhraseValidation()
object FailedChecksum : SeedPhraseValidation() object FailedChecksum : SeedPhraseValidation()
class Valid(val seedPhrase: SeedPhrase) : SeedPhraseValidation() class Valid(val seedPhrase: SeedPhrase) : SeedPhraseValidation()
companion object { companion object {

View File

@ -6,7 +6,6 @@ import cash.z.ecc.sdk.fixture.Zip321UriBuildFixture
import cash.z.ecc.sdk.fixture.Zip321UriParseFixture import cash.z.ecc.sdk.fixture.Zip321UriParseFixture
data class ZecRequest(val address: WalletAddress.Unified, val amount: Zatoshi, val message: ZecRequestMessage) { data class ZecRequest(val address: WalletAddress.Unified, val amount: Zatoshi, val message: ZecRequestMessage) {
// TODO [#397]: Waiting for an implementation of Uri parser in SDK project // TODO [#397]: Waiting for an implementation of Uri parser in SDK project
// TODO [#397]: https://github.com/Electric-Coin-Company/zcash-android-wallet-sdk/issues/397 // TODO [#397]: https://github.com/Electric-Coin-Company/zcash-android-wallet-sdk/issues/397
suspend fun toUri(): String { suspend fun toUri(): String {

View File

@ -1,4 +1,4 @@
@file:Suppress("ktlint:filename") @file:Suppress("ktlint:standard:filename")
package cash.z.ecc.sdk.type package cash.z.ecc.sdk.type
@ -21,6 +21,7 @@ import cash.z.ecc.sdk.ext.R
* - Using a ContentProvider for dynamic injection, where the URI is defined * - Using a ContentProvider for dynamic injection, where the URI is defined
* - Using AndroidManifest metadata for dynamic injection * - Using AndroidManifest metadata for dynamic injection
*/ */
/** /**
* @return Zcash network determined from resources. A resource overlay of [R.bool.zcash_is_testnet] * @return Zcash network determined from resources. A resource overlay of [R.bool.zcash_is_testnet]
* can be used for different build variants to change the network type. * can be used for different build variants to change the network type.

View File

@ -13,13 +13,16 @@ class AbstractProcessNameContentProviderTest {
@SmallTest @SmallTest
fun getProcessName_from_provider_info() { fun getProcessName_from_provider_info() {
val expectedApplicationProcessName = "beep" // $NON-NLS val expectedApplicationProcessName = "beep" // $NON-NLS
val ctx: ContextWrapper = object : ContextWrapper(ApplicationProvider.getApplicationContext()) { val ctx: ContextWrapper =
override fun getApplicationInfo() = ApplicationInfo().apply { object : ContextWrapper(ApplicationProvider.getApplicationContext()) {
override fun getApplicationInfo() =
ApplicationInfo().apply {
processName = expectedApplicationProcessName processName = expectedApplicationProcessName
} }
} }
val actualProcessName = AbstractProcessNameContentProvider.getProcessNameLegacy( val actualProcessName =
AbstractProcessNameContentProvider.getProcessNameLegacy(
ctx, ctx,
ProviderInfo() ProviderInfo()
) )

View File

@ -13,7 +13,8 @@ class VersionCodeCompatTest {
fun versionCodeCompat() { fun versionCodeCompat() {
val expectedVersionCode = 123L val expectedVersionCode = 123L
val packageInfo = PackageInfo().apply { val packageInfo =
PackageInfo().apply {
@Suppress("Deprecation") @Suppress("Deprecation")
versionCode = expectedVersionCode.toInt() versionCode = expectedVersionCode.toInt()
if (AndroidApiVersion.isAtLeastT) { if (AndroidApiVersion.isAtLeastT) {

View File

@ -11,7 +11,9 @@ object AndroidApiVersion {
* [sdk]. * [sdk].
*/ */
@ChecksSdkIntAtLeast(parameter = 0) @ChecksSdkIntAtLeast(parameter = 0)
fun isAtLeast(@IntRange(from = Build.VERSION_CODES.BASE.toLong()) sdk: Int): Boolean { fun isAtLeast(
@IntRange(from = Build.VERSION_CODES.BASE.toLong()) sdk: Int
): Boolean {
return Build.VERSION.SDK_INT >= sdk return Build.VERSION.SDK_INT >= sdk
} }

View File

@ -1,4 +1,4 @@
@file:Suppress("ktlint:filename") @file:Suppress("ktlint:standard:filename")
package co.electriccoin.zcash.spackle package co.electriccoin.zcash.spackle
@ -7,6 +7,7 @@ import android.content.ClipboardManager
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
suspend fun ClipboardManager.setPrimaryClipSuspend(data: ClipData) = withContext(Dispatchers.IO) { suspend fun ClipboardManager.setPrimaryClipSuspend(data: ClipData) =
withContext(Dispatchers.IO) {
setPrimaryClip(data) setPrimaryClip(data)
} }

View File

@ -7,7 +7,6 @@ import android.widget.Toast
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
object ClipboardManagerUtil { object ClipboardManagerUtil {
fun copyToClipboard( fun copyToClipboard(
context: Context, context: Context,
label: String, label: String,
@ -15,7 +14,8 @@ object ClipboardManagerUtil {
) { ) {
Twig.info { "Copied to clipboard: label: $label, value: $value" } Twig.info { "Copied to clipboard: label: $label, value: $value" }
val clipboardManager = context.getSystemService(ClipboardManager::class.java) val clipboardManager = context.getSystemService(ClipboardManager::class.java)
val data = ClipData.newPlainText( val data =
ClipData.newPlainText(
label, label,
value value
) )

View File

@ -1,4 +1,4 @@
@file:Suppress("ktlint:filename") @file:Suppress("ktlint:standard:filename")
package co.electriccoin.zcash.spackle package co.electriccoin.zcash.spackle
@ -6,6 +6,7 @@ import android.content.Context
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
suspend fun Context.getExternalFilesDirSuspend(type: String?) = withContext(Dispatchers.IO) { suspend fun Context.getExternalFilesDirSuspend(type: String?) =
withContext(Dispatchers.IO) {
getExternalFilesDir(type) getExternalFilesDir(type)
} }

View File

@ -11,7 +11,10 @@ import kotlinx.coroutines.launch
* It is not recommended to cancel this scope. * It is not recommended to cancel this scope.
*/ */
abstract class CoroutineBroadcastReceiver(private val broadcastReceiverScope: CoroutineScope) : BroadcastReceiver() { abstract class CoroutineBroadcastReceiver(private val broadcastReceiverScope: CoroutineScope) : BroadcastReceiver() {
final override fun onReceive(context: Context, intent: Intent) { final override fun onReceive(
context: Context,
intent: Intent
) {
val pendingResult = goAsync() val pendingResult = goAsync()
broadcastReceiverScope.launch { broadcastReceiverScope.launch {
@ -29,5 +32,8 @@ abstract class CoroutineBroadcastReceiver(private val broadcastReceiverScope: Co
* the Android timeout for broadcast receivers. This method is suitable for brief disk IO but * the Android timeout for broadcast receivers. This method is suitable for brief disk IO but
* not suitable for network calls. * not suitable for network calls.
*/ */
abstract suspend fun onReceiveSuspend(context: Context, intent: Intent) abstract suspend fun onReceiveSuspend(
context: Context,
intent: Intent
)
} }

View File

@ -11,7 +11,8 @@ object EmulatorWtfUtil {
private const val EMULATOR_WTF_SETTING = "emulator.wtf" // $NON-NLS private const val EMULATOR_WTF_SETTING = "emulator.wtf" // $NON-NLS
private const val SETTING_TRUE = "true" // $NON-NLS private const val SETTING_TRUE = "true" // $NON-NLS
private val isEmulatorWtfCached = LazyWithArgument<Context, Boolean> { private val isEmulatorWtfCached =
LazyWithArgument<Context, Boolean> {
isEmulatorWtfImpl(it) isEmulatorWtfImpl(it)
} }

View File

@ -11,7 +11,8 @@ object FirebaseTestLabUtil {
private const val FIREBASE_TEST_LAB_SETTING = "firebase.test.lab" // $NON-NLS private const val FIREBASE_TEST_LAB_SETTING = "firebase.test.lab" // $NON-NLS
private const val SETTING_TRUE = "true" // $NON-NLS private const val SETTING_TRUE = "true" // $NON-NLS
private val isFirebaseTestLabCached = LazyWithArgument<Context, Boolean> { private val isFirebaseTestLabCached =
LazyWithArgument<Context, Boolean> {
isFirebaseTestLabImpl(it) isFirebaseTestLabImpl(it)
} }
@ -24,10 +25,10 @@ object FirebaseTestLabUtil {
/* /*
* Per the documentation at https://firebase.google.com/docs/test-lab/android-studio * Per the documentation at https://firebase.google.com/docs/test-lab/android-studio
*/ */
return runCatching {
// Tested with the benchmark library, this is very fast. There shouldn't be a need to make // Tested with the benchmark library, this is very fast. There shouldn't be a need to make
// this a suspend function. That said, we'll still cache the result as a just-in-case // this a suspend function. That said, we'll still cache the result as a just-in-case
// since IPC may be involved. // since IPC may be involved.
return runCatching {
SETTING_TRUE == Settings.System.getString(context.contentResolver, FIREBASE_TEST_LAB_SETTING) SETTING_TRUE == Settings.System.getString(context.contentResolver, FIREBASE_TEST_LAB_SETTING)
}.recover { }.recover {
// Fail-safe in case an error occurs // Fail-safe in case an error occurs

View File

@ -7,14 +7,20 @@ import android.os.Build
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
fun PackageManager.getPackageInfoCompat(packageName: String, flags: Long): PackageInfo = fun PackageManager.getPackageInfoCompat(
packageName: String,
flags: Long
): PackageInfo =
if (AndroidApiVersion.isAtLeastT) { if (AndroidApiVersion.isAtLeastT) {
getPackageInfoTPlus(packageName, flags) getPackageInfoTPlus(packageName, flags)
} else { } else {
getPackageInfoLegacy(packageName, flags) getPackageInfoLegacy(packageName, flags)
} }
suspend fun PackageManager.getPackageInfoCompatSuspend(packageName: String, flags: Long): PackageInfo = suspend fun PackageManager.getPackageInfoCompatSuspend(
packageName: String,
flags: Long
): PackageInfo =
if (AndroidApiVersion.isAtLeastT) { if (AndroidApiVersion.isAtLeastT) {
withContext(Dispatchers.IO) { getPackageInfoTPlus(packageName, flags) } withContext(Dispatchers.IO) { getPackageInfoTPlus(packageName, flags) }
} else { } else {
@ -22,9 +28,13 @@ suspend fun PackageManager.getPackageInfoCompatSuspend(packageName: String, flag
} }
@TargetApi(Build.VERSION_CODES.TIRAMISU) @TargetApi(Build.VERSION_CODES.TIRAMISU)
private fun PackageManager.getPackageInfoTPlus(packageName: String, flags: Long) = private fun PackageManager.getPackageInfoTPlus(
getPackageInfo(packageName, PackageManager.PackageInfoFlags.of(flags)) packageName: String,
flags: Long
) = getPackageInfo(packageName, PackageManager.PackageInfoFlags.of(flags))
@Suppress("Deprecation") @Suppress("Deprecation")
private fun PackageManager.getPackageInfoLegacy(packageName: String, flags: Long) = private fun PackageManager.getPackageInfoLegacy(
getPackageInfo(packageName, flags.toInt()) packageName: String,
flags: Long
) = getPackageInfo(packageName, flags.toInt())

View File

@ -4,7 +4,6 @@ import android.annotation.SuppressLint
import android.os.StrictMode import android.os.StrictMode
object StrictModeCompat { object StrictModeCompat {
fun enableStrictMode(isCrashOnViolation: Boolean) { fun enableStrictMode(isCrashOnViolation: Boolean) {
configureStrictMode(isCrashOnViolation) configureStrictMode(isCrashOnViolation)
} }

View File

@ -40,7 +40,10 @@ object Twig {
// JVMStatic is to simplify ProGuard/R8 rules for stripping this // JVMStatic is to simplify ProGuard/R8 rules for stripping this
@JvmStatic @JvmStatic
fun verbose(throwable: Throwable, message: () -> String) { fun verbose(
throwable: Throwable,
message: () -> String
) {
Log.v(tag, formatMessage(message), throwable) Log.v(tag, formatMessage(message), throwable)
} }
@ -52,7 +55,10 @@ object Twig {
// JVMStatic is to simplify ProGuard/R8 rules for stripping this // JVMStatic is to simplify ProGuard/R8 rules for stripping this
@JvmStatic @JvmStatic
fun debug(throwable: Throwable, message: () -> String) { fun debug(
throwable: Throwable,
message: () -> String
) {
Log.d(tag, formatMessage(message), throwable) Log.d(tag, formatMessage(message), throwable)
} }
@ -64,7 +70,10 @@ object Twig {
// JVMStatic is to simplify ProGuard/R8 rules for stripping this // JVMStatic is to simplify ProGuard/R8 rules for stripping this
@JvmStatic @JvmStatic
fun info(throwable: Throwable, message: () -> String) { fun info(
throwable: Throwable,
message: () -> String
) {
Log.i(tag, formatMessage(message), throwable) Log.i(tag, formatMessage(message), throwable)
} }
@ -76,7 +85,10 @@ object Twig {
// JVMStatic is to simplify ProGuard/R8 rules for stripping this // JVMStatic is to simplify ProGuard/R8 rules for stripping this
@JvmStatic @JvmStatic
fun warn(throwable: Throwable, message: () -> String) { fun warn(
throwable: Throwable,
message: () -> String
) {
Log.w(tag, formatMessage(message), throwable) Log.w(tag, formatMessage(message), throwable)
} }
@ -88,7 +100,10 @@ object Twig {
// JVMStatic is to simplify ProGuard/R8 rules for stripping this // JVMStatic is to simplify ProGuard/R8 rules for stripping this
@JvmStatic @JvmStatic
fun error(throwable: Throwable, message: () -> String) { fun error(
throwable: Throwable,
message: () -> String
) {
Log.e(tag, formatMessage(message), throwable) Log.e(tag, formatMessage(message), throwable)
} }
@ -96,11 +111,12 @@ object Twig {
* Can be called in a release build to test that `assumenosideeffects` ProGuard rules have been * Can be called in a release build to test that `assumenosideeffects` ProGuard rules have been
* properly processed to strip out logging messages. * properly processed to strip out logging messages.
*/ */
// JVMStatic is to simplify ProGuard/R8 rules for stripping this @JvmStatic // JVMStatic is to simplify ProGuard/R8 rules for stripping this
@JvmStatic
fun assertLoggingStripped() { fun assertLoggingStripped() {
@Suppress("MaxLineLength") throw AssertionError(
throw AssertionError("Logging was not disabled by ProGuard or R8. Logging should be disabled in release builds to reduce risk of sensitive information being leaked.") // $NON-NLS-1$ "Logging was not disabled by ProGuard or R8. Logging should be disabled in release builds to reduce risk " +
"of sensitive information being leaked."
) // $NON-NLS-1$
} }
private const val CALL_DEPTH = 4 private const val CALL_DEPTH = 4

View File

@ -5,7 +5,8 @@ import android.content.pm.PackageInfo
import android.os.Build import android.os.Build
val PackageInfo.versionCodeCompat val PackageInfo.versionCodeCompat
get() = if (AndroidApiVersion.isAtLeastP) { get() =
if (AndroidApiVersion.isAtLeastP) {
getVersionCodePPlus() getVersionCodePPlus()
} else { } else {
versionCodeLegacy.toLong() versionCodeLegacy.toLong()

View File

@ -20,10 +20,14 @@ import co.electriccoin.zcash.spackle.AndroidApiVersion
open class AbstractProcessNameContentProvider : ContentProvider() { open class AbstractProcessNameContentProvider : ContentProvider() {
override fun onCreate() = true override fun onCreate() = true
override fun attachInfo(context: Context, info: ProviderInfo) { override fun attachInfo(
context: Context,
info: ProviderInfo
) {
super.attachInfo(context, info) super.attachInfo(context, info)
val processName: String = if (AndroidApiVersion.isAtLeastT) { val processName: String =
if (AndroidApiVersion.isAtLeastT) {
getProcessNameTPlus() getProcessNameTPlus()
} else if (AndroidApiVersion.isAtLeastP) { } else if (AndroidApiVersion.isAtLeastP) {
getProcessNamePPlus() getProcessNamePPlus()
@ -54,11 +58,18 @@ open class AbstractProcessNameContentProvider : ContentProvider() {
throw UnsupportedOperationException() throw UnsupportedOperationException()
} }
override fun insert(uri: Uri, values: ContentValues?): Uri? { override fun insert(
uri: Uri,
values: ContentValues?
): Uri? {
throw UnsupportedOperationException() throw UnsupportedOperationException()
} }
override fun delete(uri: Uri, selection: String?, selectionArgs: Array<out String>?): Int { override fun delete(
uri: Uri,
selection: String?,
selectionArgs: Array<out String>?
): Int {
throw UnsupportedOperationException() throw UnsupportedOperationException()
} }
@ -72,7 +83,9 @@ open class AbstractProcessNameContentProvider : ContentProvider() {
} }
companion object { companion object {
internal fun getProcessNameLegacy(context: Context, info: ProviderInfo) = internal fun getProcessNameLegacy(
info.processName ?: context.applicationInfo.processName ?: context.packageName context: Context,
info: ProviderInfo
) = info.processName ?: context.applicationInfo.processName ?: context.packageName
} }
} }

View File

@ -20,7 +20,6 @@ import co.electriccoin.zcash.spackle.process.ProcessNameCompat.getProcessName
* way to get process name on older Android versions. * way to get process name on older Android versions.
*/ */
object ProcessNameCompat { object ProcessNameCompat {
// GuardedBy intrinsicLock // GuardedBy intrinsicLock
private var processName: String? = null private var processName: String? = null

View File

@ -4,7 +4,6 @@ import kotlin.test.Test
import kotlin.test.assertFailsWith import kotlin.test.assertFailsWith
class ProgressTest { class ProgressTest {
@Test @Test
fun last_greater_than_zero() { fun last_greater_than_zero() {
assertFailsWith(IllegalArgumentException::class) { assertFailsWith(IllegalArgumentException::class) {

View File

@ -8,35 +8,43 @@ import java.io.File
import java.io.IOException import java.io.IOException
import java.util.UUID import java.util.UUID
suspend fun File.existsSuspend() = withContext(Dispatchers.IO) { suspend fun File.existsSuspend() =
withContext(Dispatchers.IO) {
exists() exists()
} }
suspend fun File.mkdirsSuspend() = withContext(Dispatchers.IO) { suspend fun File.mkdirsSuspend() =
withContext(Dispatchers.IO) {
mkdirs() mkdirs()
} }
suspend fun File.isDirectorySuspend() = withContext(Dispatchers.IO) { suspend fun File.isDirectorySuspend() =
withContext(Dispatchers.IO) {
isDirectory isDirectory
} }
suspend fun File.isFileSuspend() = withContext(Dispatchers.IO) { suspend fun File.isFileSuspend() =
withContext(Dispatchers.IO) {
isFile isFile
} }
suspend fun File.canWriteSuspend() = withContext(Dispatchers.IO) { suspend fun File.canWriteSuspend() =
withContext(Dispatchers.IO) {
canWrite() canWrite()
} }
suspend fun File.deleteSuspend() = withContext(Dispatchers.IO) { suspend fun File.deleteSuspend() =
withContext(Dispatchers.IO) {
delete() delete()
} }
suspend fun File.renameToSuspend(destination: File) = withContext(Dispatchers.IO) { suspend fun File.renameToSuspend(destination: File) =
withContext(Dispatchers.IO) {
renameTo(destination) renameTo(destination)
} }
suspend fun File.listFilesSuspend() = withContext(Dispatchers.IO) { suspend fun File.listFilesSuspend() =
withContext(Dispatchers.IO) {
listFiles() listFiles()
} }
@ -50,7 +58,8 @@ suspend fun File.listFilesSuspend() = withContext(Dispatchers.IO) {
* delete, rename, or do other operations in the filesystem. * delete, rename, or do other operations in the filesystem.
*/ */
suspend fun File.writeAtomically(action: (suspend (File) -> Unit)) { suspend fun File.writeAtomically(action: (suspend (File) -> Unit)) {
val tempFile = withContext(Dispatchers.IO) { val tempFile =
withContext(Dispatchers.IO) {
File(parentFile, name.newTempFileName()).also { File(parentFile, name.newTempFileName()).also {
it.deleteOnExit() it.deleteOnExit()
} }
@ -61,7 +70,9 @@ suspend fun File.writeAtomically(action: (suspend (File) -> Unit)) {
try { try {
action(tempFile) action(tempFile)
isWriteSuccessful = true isWriteSuccessful = true
} catch (@Suppress("TooGenericExceptionCaught") e: Exception) { } catch (
@Suppress("TooGenericExceptionCaught") e: Exception
) {
tempFile.deleteSuspend() tempFile.deleteSuspend()
throw e throw e
} finally { } finally {

View File

@ -16,7 +16,8 @@ class WriteAtomicallyTest {
private fun newFile() = File(File("build"), "atomic_file_test-${UUID.randomUUID()}") private fun newFile() = File(File("build"), "atomic_file_test-${UUID.randomUUID()}")
@Test @Test
fun `file has temp name`() = runTest { fun `file has temp name`() =
runTest {
val testFile = newFile() val testFile = newFile()
try { try {
testFile.writeAtomically { testFile.writeAtomically {
@ -29,7 +30,8 @@ class WriteAtomicallyTest {
} }
@Test @Test
fun `temp file deleted`() = runTest { fun `temp file deleted`() =
runTest {
val testFile = newFile() val testFile = newFile()
try { try {
var tempFile: File? = null var tempFile: File? = null
@ -47,7 +49,8 @@ class WriteAtomicallyTest {
} }
@Test @Test
fun `file is renamed`() = runTest { fun `file is renamed`() =
runTest {
val testFile = newFile() val testFile = newFile()
try { try {
testFile.writeAtomically { testFile.writeAtomically {

View File

@ -10,8 +10,9 @@ import org.junit.Before
* Subclass this in view unit and integration tests. This verifies that * Subclass this in view unit and integration tests. This verifies that
* prerequisites necessary for reliable UI tests are met, and it provides more useful error messages. * prerequisites necessary for reliable UI tests are met, and it provides more useful error messages.
*/ */
// Originally hoped to put this into ZcashUiTestRunner, although it causes reporting of test results to fail
open class UiTestPrerequisites { open class UiTestPrerequisites {
// Originally hoped to put this into ZcashUiTestRunner, although it causes reporting of test results to fail
@Before @Before
fun verifyPrerequisites() { fun verifyPrerequisites() {
assertScreenIsOn() assertScreenIsOn()
@ -26,7 +27,8 @@ open class UiTestPrerequisites {
} }
private fun isScreenOn(): Boolean { private fun isScreenOn(): Boolean {
val powerService = ApplicationProvider.getApplicationContext<Context>() val powerService =
ApplicationProvider.getApplicationContext<Context>()
.getSystemService(Context.POWER_SERVICE) as PowerManager .getSystemService(Context.POWER_SERVICE) as PowerManager
return powerService.isInteractive return powerService.isInteractive
} }

View File

@ -12,7 +12,8 @@ class ZcashUiTestRunner : AndroidJUnitRunner() {
override fun onCreate(arguments: Bundle?) { override fun onCreate(arguments: Bundle?) {
super.onCreate(arguments) super.onCreate(arguments)
val powerManager = ApplicationProvider.getApplicationContext<Context>() val powerManager =
ApplicationProvider.getApplicationContext<Context>()
.getSystemService(Context.POWER_SERVICE) as PowerManager .getSystemService(Context.POWER_SERVICE) as PowerManager
// There is no alternative to this deprecated API. The suggestion of a view to keep the screen // There is no alternative to this deprecated API. The suggestion of a view to keep the screen

View File

@ -3,3 +3,5 @@ root = true
[*.{kt,kts}] [*.{kt,kts}]
ktlint_standard_trailing-comma-on-call-site = disabled ktlint_standard_trailing-comma-on-call-site = disabled
ktlint_standard_trailing-comma-on-declaration-site = disabled ktlint_standard_trailing-comma-on-declaration-site = disabled
# When using Compose, suppress the `function-naming` rule in favor of PascalCase naming convention
ktlint_function_naming_ignore_when_annotated_with=Composable

View File

@ -18,7 +18,6 @@ import org.junit.Test
* version and later on. * version and later on.
*/ */
class BasicStartupBenchmark { class BasicStartupBenchmark {
companion object { companion object {
private const val APP_TARGET_PACKAGE_NAME = "co.electriccoin.zcash" private const val APP_TARGET_PACKAGE_NAME = "co.electriccoin.zcash"
} }
@ -27,7 +26,8 @@ class BasicStartupBenchmark {
val benchmarkRule = MacrobenchmarkRule() val benchmarkRule = MacrobenchmarkRule()
@Test @Test
fun startup() = benchmarkRule.measureRepeated( fun startup() =
benchmarkRule.measureRepeated(
packageName = APP_TARGET_PACKAGE_NAME, packageName = APP_TARGET_PACKAGE_NAME,
metrics = listOf(StartupTimingMetric()), metrics = listOf(StartupTimingMetric()),
iterations = 5, iterations = 5,

View File

@ -59,7 +59,8 @@ fun PrimaryButton(
onClick: () -> Unit, onClick: () -> Unit,
text: String, text: String,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
outerPaddingValues: PaddingValues = PaddingValues( outerPaddingValues: PaddingValues =
PaddingValues(
horizontal = ZcashTheme.dimens.spacingNone, horizontal = ZcashTheme.dimens.spacingNone,
vertical = ZcashTheme.dimens.spacingSmall vertical = ZcashTheme.dimens.spacingSmall
), ),
@ -70,7 +71,8 @@ fun PrimaryButton(
Button( Button(
shape = RectangleShape, shape = RectangleShape,
enabled = enabled, enabled = enabled,
modifier = modifier.then(Modifier.fillMaxWidth()) modifier =
modifier.then(Modifier.fillMaxWidth())
.padding(outerPaddingValues) .padding(outerPaddingValues)
.shadow( .shadow(
contentColor = textColor, contentColor = textColor,
@ -81,12 +83,14 @@ fun PrimaryButton(
spread = ZcashTheme.dimens.buttonShadowSpread, spread = ZcashTheme.dimens.buttonShadowSpread,
) )
.translationClick( .translationClick(
translationX = ZcashTheme.dimens.buttonShadowOffsetX + 6.dp, // + 6dp to exactly cover the bottom shadow // + 6dp to exactly cover the bottom shadow
translationX = ZcashTheme.dimens.buttonShadowOffsetX + 6.dp,
translationY = ZcashTheme.dimens.buttonShadowOffsetX + 6.dp translationY = ZcashTheme.dimens.buttonShadowOffsetX + 6.dp
) )
.defaultMinSize(ZcashTheme.dimens.buttonWidth, ZcashTheme.dimens.buttonHeight) .defaultMinSize(ZcashTheme.dimens.buttonWidth, ZcashTheme.dimens.buttonHeight)
.border(1.dp, Color.Black), .border(1.dp, Color.Black),
colors = buttonColors( colors =
buttonColors(
containerColor = buttonColor, containerColor = buttonColor,
disabledContainerColor = ZcashTheme.colors.disabledButtonColor, disabledContainerColor = ZcashTheme.colors.disabledButtonColor,
disabledContentColor = ZcashTheme.colors.disabledButtonTextColor disabledContentColor = ZcashTheme.colors.disabledButtonTextColor
@ -108,7 +112,8 @@ fun SecondaryButton(
onClick: () -> Unit, onClick: () -> Unit,
text: String, text: String,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
outerPaddingValues: PaddingValues = PaddingValues( outerPaddingValues: PaddingValues =
PaddingValues(
horizontal = ZcashTheme.dimens.spacingNone, horizontal = ZcashTheme.dimens.spacingNone,
vertical = ZcashTheme.dimens.spacingSmall vertical = ZcashTheme.dimens.spacingSmall
), ),
@ -119,7 +124,8 @@ fun SecondaryButton(
Button( Button(
shape = RectangleShape, shape = RectangleShape,
enabled = enabled, enabled = enabled,
modifier = modifier.then(Modifier.fillMaxWidth()) modifier =
modifier.then(Modifier.fillMaxWidth())
.padding(outerPaddingValues) .padding(outerPaddingValues)
.shadow( .shadow(
contentColor = textColor, contentColor = textColor,
@ -129,12 +135,14 @@ fun SecondaryButton(
spread = ZcashTheme.dimens.buttonShadowSpread, spread = ZcashTheme.dimens.buttonShadowSpread,
) )
.translationClick( .translationClick(
translationX = ZcashTheme.dimens.buttonShadowOffsetX + 6.dp, // + 6dp to exactly cover the bottom shadow // + 6dp to exactly cover the bottom shadow
translationX = ZcashTheme.dimens.buttonShadowOffsetX + 6.dp,
translationY = ZcashTheme.dimens.buttonShadowOffsetX + 6.dp translationY = ZcashTheme.dimens.buttonShadowOffsetX + 6.dp
) )
.defaultMinSize(ZcashTheme.dimens.buttonWidth, ZcashTheme.dimens.buttonHeight) .defaultMinSize(ZcashTheme.dimens.buttonWidth, ZcashTheme.dimens.buttonHeight)
.border(1.dp, Color.Black), .border(1.dp, Color.Black),
colors = buttonColors( colors =
buttonColors(
containerColor = buttonColor, containerColor = buttonColor,
disabledContainerColor = ZcashTheme.colors.disabledButtonColor, disabledContainerColor = ZcashTheme.colors.disabledButtonColor,
disabledContentColor = ZcashTheme.colors.disabledButtonTextColor disabledContentColor = ZcashTheme.colors.disabledButtonTextColor
@ -155,7 +163,8 @@ fun NavigationButton(
onClick: () -> Unit, onClick: () -> Unit,
text: String, text: String,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
outerPaddingValues: PaddingValues = PaddingValues( outerPaddingValues: PaddingValues =
PaddingValues(
horizontal = ZcashTheme.dimens.spacingDefault, horizontal = ZcashTheme.dimens.spacingDefault,
vertical = ZcashTheme.dimens.spacingSmall vertical = ZcashTheme.dimens.spacingSmall
), ),
@ -163,7 +172,8 @@ fun NavigationButton(
Button( Button(
shape = RectangleShape, shape = RectangleShape,
onClick = onClick, onClick = onClick,
modifier = modifier.then( modifier =
modifier.then(
Modifier Modifier
.padding(outerPaddingValues) .padding(outerPaddingValues)
), ),
@ -183,7 +193,8 @@ fun TertiaryButton(
onClick: () -> Unit, onClick: () -> Unit,
text: String, text: String,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
outerPaddingValues: PaddingValues = PaddingValues( outerPaddingValues: PaddingValues =
PaddingValues(
horizontal = ZcashTheme.dimens.spacingDefault, horizontal = ZcashTheme.dimens.spacingDefault,
vertical = ZcashTheme.dimens.spacingSmall vertical = ZcashTheme.dimens.spacingSmall
), ),
@ -192,7 +203,8 @@ fun TertiaryButton(
Button( Button(
shape = RectangleShape, shape = RectangleShape,
onClick = onClick, onClick = onClick,
modifier = modifier.then( modifier =
modifier.then(
Modifier Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(outerPaddingValues) .padding(outerPaddingValues)
@ -215,7 +227,8 @@ fun DangerousButton(
onClick: () -> Unit, onClick: () -> Unit,
text: String, text: String,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
outerPaddingValues: PaddingValues = PaddingValues( outerPaddingValues: PaddingValues =
PaddingValues(
horizontal = ZcashTheme.dimens.spacingDefault, horizontal = ZcashTheme.dimens.spacingDefault,
vertical = ZcashTheme.dimens.spacingSmall vertical = ZcashTheme.dimens.spacingSmall
), ),
@ -223,7 +236,8 @@ fun DangerousButton(
Button( Button(
shape = RectangleShape, shape = RectangleShape,
onClick = onClick, onClick = onClick,
modifier = modifier.then( modifier =
modifier.then(
Modifier Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(outerPaddingValues) .padding(outerPaddingValues)
@ -291,6 +305,7 @@ fun Modifier.shadow(
) )
private enum class ButtonState { Pressed, Idle } private enum class ButtonState { Pressed, Idle }
fun Modifier.translationClick( fun Modifier.translationClick(
translationX: Dp = 0.dp, translationX: Dp = 0.dp,
translationY: Dp = 0.dp translationY: Dp = 0.dp
@ -298,24 +313,28 @@ fun Modifier.translationClick(
var buttonState by remember { mutableStateOf(ButtonState.Idle) } var buttonState by remember { mutableStateOf(ButtonState.Idle) }
val translationXAnimated by animateFloatAsState( val translationXAnimated by animateFloatAsState(
targetValue = if (buttonState == ButtonState.Pressed) { targetValue =
if (buttonState == ButtonState.Pressed) {
translationX.value translationX.value
} else { } else {
0f 0f
}, },
label = "ClickTranslationXAnimation", label = "ClickTranslationXAnimation",
animationSpec = tween( animationSpec =
tween(
durationMillis = 100 durationMillis = 100
) )
) )
val translationYAnimated by animateFloatAsState( val translationYAnimated by animateFloatAsState(
targetValue = if (buttonState == ButtonState.Pressed) { targetValue =
if (buttonState == ButtonState.Pressed) {
translationY.value translationY.value
} else { } else {
0f 0f
}, },
label = "ClickTranslationYAnimation", label = "ClickTranslationYAnimation",
animationSpec = tween( animationSpec =
tween(
durationMillis = 100 durationMillis = 100
) )
) )
@ -327,7 +346,8 @@ fun Modifier.translationClick(
} }
.pointerInput(buttonState) { .pointerInput(buttonState) {
awaitPointerEventScope { awaitPointerEventScope {
buttonState = if (buttonState == ButtonState.Pressed) { buttonState =
if (buttonState == ButtonState.Pressed) {
waitForUpOrCancellation() waitForUpOrCancellation()
ButtonState.Idle ButtonState.Idle
} else { } else {

View File

@ -41,7 +41,8 @@ fun CheckBox(
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
modifier = modifier modifier = modifier
) { ) {
val checkBoxModifier = Modifier val checkBoxModifier =
Modifier
.padding( .padding(
top = ZcashTheme.dimens.spacingTiny, top = ZcashTheme.dimens.spacingTiny,
bottom = ZcashTheme.dimens.spacingTiny, bottom = ZcashTheme.dimens.spacingTiny,

View File

@ -85,10 +85,12 @@ fun ChipOnSurface(
) { ) {
Surface( Surface(
shape = RectangleShape, shape = RectangleShape,
modifier = modifier modifier =
modifier
.padding(horizontal = ZcashTheme.dimens.spacingTiny) .padding(horizontal = ZcashTheme.dimens.spacingTiny)
.border( .border(
border = BorderStroke( border =
BorderStroke(
width = ZcashTheme.dimens.chipStroke, width = ZcashTheme.dimens.chipStroke,
color = ZcashTheme.colors.layoutStroke color = ZcashTheme.colors.layoutStroke
) )
@ -100,7 +102,8 @@ fun ChipOnSurface(
text = text, text = text,
style = MaterialTheme.typography.bodyLarge, style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.onSecondary, color = MaterialTheme.colorScheme.onSecondary,
modifier = Modifier modifier =
Modifier
.padding( .padding(
vertical = ZcashTheme.dimens.spacingSmall, vertical = ZcashTheme.dimens.spacingSmall,
horizontal = ZcashTheme.dimens.spacingDefault horizontal = ZcashTheme.dimens.spacingDefault

View File

@ -48,11 +48,13 @@ fun ChipGrid(
horizontalArrangement = Arrangement.Center horizontalArrangement = Arrangement.Center
) { ) {
Row( Row(
modifier = Modifier modifier =
Modifier
.wrapContentWidth() .wrapContentWidth()
.clickable( .clickable(
interactionSource = interactionSource, interactionSource = interactionSource,
indication = null, // Disable ripple // Disable ripple
indication = null,
onClick = onGridClick onClick = onGridClick
) )
.testTag(CommonTag.CHIP_LAYOUT) .testTag(CommonTag.CHIP_LAYOUT)

View File

@ -9,10 +9,14 @@ import androidx.compose.ui.graphics.RectangleShape
import co.electriccoin.zcash.ui.design.theme.ZcashTheme import co.electriccoin.zcash.ui.design.theme.ZcashTheme
@Composable @Composable
fun GradientSurface(modifier: Modifier = Modifier, content: @Composable () -> Unit) { fun GradientSurface(
modifier: Modifier = Modifier,
content: @Composable () -> Unit
) {
Surface( Surface(
color = Color.Transparent, color = Color.Transparent,
modifier = modifier modifier =
modifier
.background(ZcashTheme.colors.surfaceGradient()), .background(ZcashTheme.colors.surfaceGradient()),
shape = RectangleShape, shape = RectangleShape,
content = content content = content

View File

@ -15,7 +15,10 @@ import kotlinx.coroutines.flow.StateFlow
* for automated tests. * for automated tests.
*/ */
@Composable @Composable
fun Override(configurationOverrideFlow: StateFlow<ConfigurationOverride?>, content: @Composable () -> Unit) { fun Override(
configurationOverrideFlow: StateFlow<ConfigurationOverride?>,
content: @Composable () -> Unit
) {
val configurationOverride = configurationOverrideFlow.collectAsState().value val configurationOverride = configurationOverrideFlow.collectAsState().value
if (null == configurationOverride) { if (null == configurationOverride) {
@ -23,7 +26,8 @@ fun Override(configurationOverrideFlow: StateFlow<ConfigurationOverride?>, conte
} else { } else {
val configuration = configurationOverride.newConfiguration(LocalConfiguration.current) val configuration = configurationOverride.newConfiguration(LocalConfiguration.current)
val contextWrapper = run { val contextWrapper =
run {
val context = LocalContext.current val context = LocalContext.current
object : ContextThemeWrapper() { object : ContextThemeWrapper() {
init { init {
@ -43,7 +47,8 @@ fun Override(configurationOverrideFlow: StateFlow<ConfigurationOverride?>, conte
} }
data class ConfigurationOverride(val uiMode: UiMode?, val locale: LocaleList?) { data class ConfigurationOverride(val uiMode: UiMode?, val locale: LocaleList?) {
fun newConfiguration(fromConfiguration: Configuration) = Configuration(fromConfiguration).apply { fun newConfiguration(fromConfiguration: Configuration) =
Configuration(fromConfiguration).apply {
this@ConfigurationOverride.uiMode?.let { this@ConfigurationOverride.uiMode?.let {
uiMode = (uiMode and Configuration.UI_MODE_NIGHT_MASK.inv()) or it.flag() uiMode = (uiMode and Configuration.UI_MODE_NIGHT_MASK.inv()) or it.flag()
} }
@ -59,7 +64,8 @@ enum class UiMode {
Dark Dark
} }
private fun UiMode.flag() = when (this) { private fun UiMode.flag() =
when (this) {
UiMode.Light -> Configuration.UI_MODE_NIGHT_NO UiMode.Light -> Configuration.UI_MODE_NIGHT_NO
UiMode.Dark -> Configuration.UI_MODE_NIGHT_YES UiMode.Dark -> Configuration.UI_MODE_NIGHT_YES
} }

View File

@ -24,10 +24,12 @@ fun SwitchWithLabel(
val interactionSource = remember { MutableInteractionSource() } val interactionSource = remember { MutableInteractionSource() }
ConstraintLayout( ConstraintLayout(
modifier = modifier modifier =
modifier
.clickable( .clickable(
interactionSource = interactionSource, interactionSource = interactionSource,
indication = null, // disable ripple // disable ripple
indication = null,
role = Role.Switch, role = Role.Switch,
onClick = { onStateChange(!state) } onClick = { onStateChange(!state) }
) )
@ -36,7 +38,8 @@ fun SwitchWithLabel(
val (text, spacer, switchButton) = createRefs() val (text, spacer, switchButton) = createRefs()
Body( Body(
text = label, text = label,
modifier = Modifier.constrainAs(text) { modifier =
Modifier.constrainAs(text) {
top.linkTo(parent.top) top.linkTo(parent.top)
bottom.linkTo(parent.top) bottom.linkTo(parent.top)
start.linkTo(parent.start) start.linkTo(parent.start)
@ -45,7 +48,8 @@ fun SwitchWithLabel(
} }
) )
Spacer( Spacer(
modifier = Modifier modifier =
Modifier
.width(ZcashTheme.dimens.spacingDefault) .width(ZcashTheme.dimens.spacingDefault)
.constrainAs(spacer) { .constrainAs(spacer) {
top.linkTo(parent.top) top.linkTo(parent.top)
@ -59,7 +63,8 @@ fun SwitchWithLabel(
onCheckedChange = { onCheckedChange = {
onStateChange(it) onStateChange(it)
}, },
modifier = Modifier.constrainAs(switchButton) { modifier =
Modifier.constrainAs(switchButton) {
top.linkTo(parent.top) top.linkTo(parent.top)
bottom.linkTo(parent.top) bottom.linkTo(parent.top)
start.linkTo(spacer.end) start.linkTo(spacer.end)

View File

@ -147,14 +147,16 @@ fun Reference(
onClick: () -> Unit onClick: () -> Unit
) { ) {
Box( Box(
modifier = Modifier modifier =
Modifier
.wrapContentSize() .wrapContentSize()
.clip(RoundedCornerShape(ZcashTheme.dimens.topAppBarActionRippleCorner)) .clip(RoundedCornerShape(ZcashTheme.dimens.topAppBarActionRippleCorner))
.clickable { onClick() } .clickable { onClick() }
) { ) {
Text( Text(
text = text, text = text,
style = MaterialTheme.typography.bodyLarge style =
MaterialTheme.typography.bodyLarge
.merge( .merge(
TextStyle( TextStyle(
color = ZcashTheme.colors.reference, color = ZcashTheme.colors.reference,

View File

@ -27,7 +27,8 @@ fun FormTextField(
leadingIcon: @Composable (() -> Unit)? = null, leadingIcon: @Composable (() -> Unit)? = null,
trailingIcon: @Composable (() -> Unit)? = null, trailingIcon: @Composable (() -> Unit)? = null,
keyboardOptions: KeyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Text), keyboardOptions: KeyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Text),
colors: TextFieldColors = TextFieldDefaults.colors( colors: TextFieldColors =
TextFieldDefaults.colors(
focusedContainerColor = Color.Transparent, focusedContainerColor = Color.Transparent,
unfocusedContainerColor = Color.Transparent, unfocusedContainerColor = Color.Transparent,
disabledContainerColor = Color.Transparent, disabledContainerColor = Color.Transparent,
@ -35,7 +36,8 @@ fun FormTextField(
), ),
keyboardActions: KeyboardActions = KeyboardActions.Default, keyboardActions: KeyboardActions = KeyboardActions.Default,
shape: Shape = TextFieldDefaults.shape, shape: Shape = TextFieldDefaults.shape,
withBorder: Boolean = true, // To enable border around the TextField // To enable border around the TextField
withBorder: Boolean = true,
) { ) {
TextField( TextField(
value = value, value = value,
@ -44,7 +46,8 @@ fun FormTextField(
textStyle = textStyle, textStyle = textStyle,
keyboardOptions = keyboardOptions, keyboardOptions = keyboardOptions,
colors = colors, colors = colors,
modifier = modifier.then( modifier =
modifier.then(
if (withBorder) { if (withBorder) {
modifier.border(width = 1.dp, color = MaterialTheme.colorScheme.primary) modifier.border(width = 1.dp, color = MaterialTheme.colorScheme.primary)
} else { } else {

View File

@ -181,7 +181,8 @@ private fun TopBarOneVisibleActionMenuExample(
text = "Action 1", text = "Action 1",
onClick = actionCallback, onClick = actionCallback,
textAlign = TextAlign.Center, textAlign = TextAlign.Center,
modifier = modifier.then( modifier =
modifier.then(
Modifier.padding(all = ZcashTheme.dimens.spacingDefault) Modifier.padding(all = ZcashTheme.dimens.spacingDefault)
) )
) )
@ -218,7 +219,8 @@ fun SmallTopAppBar(
navigationIcon = { navigationIcon = {
backText?.let { backText?.let {
Box( Box(
modifier = Modifier modifier =
Modifier
.wrapContentSize() .wrapContentSize()
.clip(RoundedCornerShape(ZcashTheme.dimens.topAppBarActionRippleCorner)) .clip(RoundedCornerShape(ZcashTheme.dimens.topAppBarActionRippleCorner))
.clickable { onBack?.run { onBack() } } .clickable { onBack?.run { onBack() } }

View File

@ -18,30 +18,23 @@ data class Dimens(
val spacingLarge: Dp, val spacingLarge: Dp,
val spacingXlarge: Dp, val spacingXlarge: Dp,
val spacingHuge: Dp, val spacingHuge: Dp,
// List of custom spacings: // List of custom spacings:
// Button: // Button:
val buttonShadowOffsetX: Dp, val buttonShadowOffsetX: Dp,
val buttonShadowOffsetY: Dp, val buttonShadowOffsetY: Dp,
val buttonShadowSpread: Dp, val buttonShadowSpread: Dp,
val buttonWidth: Dp, val buttonWidth: Dp,
val buttonHeight: Dp, val buttonHeight: Dp,
// Chip // Chip
val chipShadowElevation: Dp, val chipShadowElevation: Dp,
val chipStroke: Dp, val chipStroke: Dp,
// TopAppBar: // TopAppBar:
val topAppBarZcashLogoHeight: Dp, val topAppBarZcashLogoHeight: Dp,
val topAppBarActionRippleCorner: Dp, val topAppBarActionRippleCorner: Dp,
// TextField: // TextField:
val textFieldDefaultHeight: Dp, val textFieldDefaultHeight: Dp,
// Any Layout: // Any Layout:
val layoutStroke: Dp, val layoutStroke: Dp,
// Screen custom spacings: // Screen custom spacings:
val inScreenZcashLogoHeight: Dp, val inScreenZcashLogoHeight: Dp,
val inScreenZcashLogoWidth: Dp, val inScreenZcashLogoWidth: Dp,
@ -49,7 +42,8 @@ data class Dimens(
val screenHorizontalSpacing: Dp, val screenHorizontalSpacing: Dp,
) )
private val defaultDimens = Dimens( private val defaultDimens =
Dimens(
spacingNone = 0.dp, spacingNone = 0.dp,
spacingXtiny = 2.dp, spacingXtiny = 2.dp,
spacingTiny = 4.dp, spacingTiny = 4.dp,
@ -77,7 +71,7 @@ private val defaultDimens = Dimens(
private val normalDimens = defaultDimens private val normalDimens = defaultDimens
internal var LocalDimens = staticCompositionLocalOf { defaultDimens } internal var localDimens = staticCompositionLocalOf { defaultDimens }
/** /**
* This is a convenience way on how to provide device specification based spacings. We use Configuration from Compose * This is a convenience way on how to provide device specification based spacings. We use Configuration from Compose
@ -119,7 +113,7 @@ internal var LocalDimens = staticCompositionLocalOf { defaultDimens }
* - rounded/normal screen shape * - rounded/normal screen shape
*/ */
@Composable @Composable
internal fun ProvideDimens(content: @Composable () -> Unit,) { internal fun ProvideDimens(content: @Composable () -> Unit) {
val resultDimens = normalDimens val resultDimens = normalDimens
CompositionLocalProvider(LocalDimens provides resultDimens, content = content) CompositionLocalProvider(localDimens provides resultDimens, content = content)
} }

View File

@ -37,8 +37,10 @@ data class ExtendedColors(
val welcomeAnimationColor: Color, val welcomeAnimationColor: Color,
) { ) {
@Composable @Composable
fun surfaceGradient() = Brush.verticalGradient( fun surfaceGradient() =
colors = listOf( Brush.verticalGradient(
colors =
listOf(
MaterialTheme.colorScheme.surface, MaterialTheme.colorScheme.surface,
ZcashTheme.colors.surfaceEnd ZcashTheme.colors.surfaceEnd
) )

View File

@ -31,13 +31,15 @@ fun ZcashTheme(
// IS_APP_DARK_MODE_ENABLED, whether the device's system dark mode is on or off. // IS_APP_DARK_MODE_ENABLED, whether the device's system dark mode is on or off.
val useDarkMode = forceDarkMode || (BuildConfig.IS_APP_DARK_MODE_ENABLED && isSystemInDarkTheme()) val useDarkMode = forceDarkMode || (BuildConfig.IS_APP_DARK_MODE_ENABLED && isSystemInDarkTheme())
val baseColors = if (useDarkMode) { val baseColors =
if (useDarkMode) {
DarkColorPalette DarkColorPalette
} else { } else {
LightColorPalette LightColorPalette
} }
val extendedColors = if (useDarkMode) { val extendedColors =
if (useDarkMode) {
DarkExtendedColorPalette DarkExtendedColorPalette
} else { } else {
LightExtendedColorPalette LightExtendedColorPalette
@ -72,5 +74,5 @@ object ZcashTheme {
// TODO [#808]: https://github.com/Electric-Coin-Company/zashi-android/issues/808 // TODO [#808]: https://github.com/Electric-Coin-Company/zashi-android/issues/808
val dimens: Dimens val dimens: Dimens
@Composable @Composable
get() = LocalDimens.current get() = localDimens.current
} }

Some files were not shown because too many files have changed in this diff Show More