From 417fc4b8a599a6844ef972e637e305a5a51b2fc0 Mon Sep 17 00:00:00 2001 From: Carter Jernigan Date: Mon, 20 Feb 2023 11:07:26 -0500 Subject: [PATCH] [#764] Android Remote Config implementation Note that there is no cloud integration yet. This creates the abstractions to support injecting different remote config sources --- ...ration_impl_android_lib_connectedCheck.xml | 53 +++++++++++++++++++ .../api/MergingConfigurationProvider.kt | 5 +- .../build.gradle.kts | 39 ++++++++++++++ .../proguard-consumer.txt | 0 .../src/androidTest/AndroidManifest.xml | 14 +++++ .../internal/intent/IntentProviderTest.kt | 27 ++++++++++ .../src/debug/AndroidManifest.xml | 12 +++++ .../src/main/AndroidManifest.xml | 6 +++ .../AndroidConfigurationFactory.kt | 33 ++++++++++++ .../internal/intent/ConfigurationIntent.kt | 7 +++ .../intent/IntentConfigurationProvider.kt | 30 +++++++++++ .../intent/IntentConfigurationReceiver.kt | 38 +++++++++++++ docs/Architecture.md | 23 ++++++-- docs/testing/manual_testing/Release build.md | 11 ++++ settings.gradle.kts | 1 + ui-lib/build.gradle.kts | 2 + .../configuration/ConfigurationEntriesTest.kt | 26 +++++++++ .../preference/EncryptedPreferenceKeysTest.kt | 10 ++-- .../preference/StandardPreferenceKeysTest.kt | 26 +++++++++ .../zcash/ui/screen/about/AboutViewTest.kt | 16 ++++-- .../co/electriccoin/zcash/ui/MainActivity.kt | 52 ++++++++++++------ .../co/electriccoin/zcash/ui/Navigation.kt | 6 ++- .../ui/configuration/ConfigurationEntries.kt | 14 +++++ .../ui/configuration/ConfigurationLocal.kt | 8 +++ .../zcash/ui/fixture/ConfigInfoFixture.kt | 15 ++++++ .../zcash/ui/screen/about/AndroidAboutView.kt | 11 +++- .../zcash/ui/screen/about/view/AboutView.kt | 21 ++++++-- .../ui/screen/home/viewmodel/HomeViewModel.kt | 5 ++ .../zcash/ui/screen/support/model/AppInfo.kt | 2 +- .../ui/screen/support/model/ConfigInfo.kt | 17 ++++++ .../ui/screen/support/model/CrashInfo.kt | 2 +- .../ui/screen/support/model/DeviceInfo.kt | 2 +- .../screen/support/model/EnvironmentInfo.kt | 2 +- .../support/model/OperatingSystemInfo.kt | 2 +- .../ui/screen/support/model/PermissionInfo.kt | 2 +- .../ui/screen/support/model/SupportInfo.kt | 14 +++-- .../zcash/ui/screen/support/model/TimeInfo.kt | 2 +- .../src/main/res/ui/about/values/strings.xml | 1 + 38 files changed, 511 insertions(+), 46 deletions(-) create mode 100644 .idea/runConfigurations/configuration_impl_android_lib_connectedCheck.xml create mode 100644 configuration-impl-android-lib/build.gradle.kts create mode 100644 configuration-impl-android-lib/proguard-consumer.txt create mode 100644 configuration-impl-android-lib/src/androidTest/AndroidManifest.xml create mode 100644 configuration-impl-android-lib/src/androidTest/java/co/electriccoin/zcash/configuration/internal/intent/IntentProviderTest.kt create mode 100644 configuration-impl-android-lib/src/debug/AndroidManifest.xml create mode 100644 configuration-impl-android-lib/src/main/AndroidManifest.xml create mode 100644 configuration-impl-android-lib/src/main/java/co/electriccoin/zcash/configuration/AndroidConfigurationFactory.kt create mode 100644 configuration-impl-android-lib/src/main/java/co/electriccoin/zcash/configuration/internal/intent/ConfigurationIntent.kt create mode 100644 configuration-impl-android-lib/src/main/java/co/electriccoin/zcash/configuration/internal/intent/IntentConfigurationProvider.kt create mode 100644 configuration-impl-android-lib/src/main/java/co/electriccoin/zcash/configuration/internal/intent/IntentConfigurationReceiver.kt create mode 100644 docs/testing/manual_testing/Release build.md create mode 100644 ui-lib/src/androidTest/java/co/electriccoin/zcash/ui/configuration/ConfigurationEntriesTest.kt create mode 100644 ui-lib/src/androidTest/java/co/electriccoin/zcash/ui/preference/StandardPreferenceKeysTest.kt create mode 100644 ui-lib/src/main/java/co/electriccoin/zcash/ui/configuration/ConfigurationEntries.kt create mode 100644 ui-lib/src/main/java/co/electriccoin/zcash/ui/configuration/ConfigurationLocal.kt create mode 100644 ui-lib/src/main/java/co/electriccoin/zcash/ui/fixture/ConfigInfoFixture.kt create mode 100644 ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/support/model/ConfigInfo.kt diff --git a/.idea/runConfigurations/configuration_impl_android_lib_connectedCheck.xml b/.idea/runConfigurations/configuration_impl_android_lib_connectedCheck.xml new file mode 100644 index 00000000..a0d7871b --- /dev/null +++ b/.idea/runConfigurations/configuration_impl_android_lib_connectedCheck.xml @@ -0,0 +1,53 @@ + + + + + \ No newline at end of file diff --git a/configuration-api-lib/src/commonMain/kotlin/co/electriccoin/zcash/configuration/api/MergingConfigurationProvider.kt b/configuration-api-lib/src/commonMain/kotlin/co/electriccoin/zcash/configuration/api/MergingConfigurationProvider.kt index 6f11329b..f2a8fa8c 100644 --- a/configuration-api-lib/src/commonMain/kotlin/co/electriccoin/zcash/configuration/api/MergingConfigurationProvider.kt +++ b/configuration-api-lib/src/commonMain/kotlin/co/electriccoin/zcash/configuration/api/MergingConfigurationProvider.kt @@ -3,6 +3,7 @@ package co.electriccoin.zcash.configuration.api import co.electriccoin.zcash.configuration.model.entry.ConfigKey import co.electriccoin.zcash.configuration.model.map.Configuration import kotlinx.collections.immutable.PersistentList +import kotlinx.collections.immutable.toPersistentList import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine import kotlinx.datetime.Instant @@ -14,7 +15,7 @@ class MergingConfigurationProvider(private val configurationProviders: Persisten override fun getConfigurationFlow(): Flow { return combine(configurationProviders.map { it.getConfigurationFlow() }) { configurations -> - MergingConfiguration(configurations.toList()) + MergingConfiguration(configurations.toList().toPersistentList()) } } @@ -23,7 +24,7 @@ class MergingConfigurationProvider(private val configurationProviders: Persisten } } -private data class MergingConfiguration(private val configurations: List) : Configuration { +private data class MergingConfiguration(private val configurations: PersistentList) : Configuration { override val updatedAt: Instant? get() = configurations.mapNotNull { it.updatedAt }.maxOrNull() diff --git a/configuration-impl-android-lib/build.gradle.kts b/configuration-impl-android-lib/build.gradle.kts new file mode 100644 index 00000000..13d7671d --- /dev/null +++ b/configuration-impl-android-lib/build.gradle.kts @@ -0,0 +1,39 @@ +plugins { + id("com.android.library") + kotlin("android") + id("secant.android-build-conventions") + id("wtf.emulator.gradle") + id("secant.emulator-wtf-conventions") + id("secant.jacoco-conventions") +} + +android { + namespace = "co.electriccoin.zcash.configuration" +} + +dependencies { + implementation(libs.kotlin.stdlib) + implementation(libs.kotlinx.coroutines.android) + implementation(libs.kotlinx.coroutines.core) + implementation(libs.kotlinx.datetime) + implementation(libs.kotlinx.immutable) + api(projects.configurationApiLib) + implementation(projects.spackleLib) + + androidTestImplementation(libs.bundles.androidx.test) + androidTestImplementation(libs.kotlinx.coroutines.test) + + androidTestUtil(libs.androidx.test.services) { + artifact { + type = "apk" + } + } + + if (project.property("IS_USE_TEST_ORCHESTRATOR").toString().toBoolean()) { + androidTestUtil(libs.androidx.test.orchestrator) { + artifact { + type = "apk" + } + } + } +} diff --git a/configuration-impl-android-lib/proguard-consumer.txt b/configuration-impl-android-lib/proguard-consumer.txt new file mode 100644 index 00000000..e69de29b diff --git a/configuration-impl-android-lib/src/androidTest/AndroidManifest.xml b/configuration-impl-android-lib/src/androidTest/AndroidManifest.xml new file mode 100644 index 00000000..756affdb --- /dev/null +++ b/configuration-impl-android-lib/src/androidTest/AndroidManifest.xml @@ -0,0 +1,14 @@ + + + + + + + + + + diff --git a/configuration-impl-android-lib/src/androidTest/java/co/electriccoin/zcash/configuration/internal/intent/IntentProviderTest.kt b/configuration-impl-android-lib/src/androidTest/java/co/electriccoin/zcash/configuration/internal/intent/IntentProviderTest.kt new file mode 100644 index 00000000..1ac922d3 --- /dev/null +++ b/configuration-impl-android-lib/src/androidTest/java/co/electriccoin/zcash/configuration/internal/intent/IntentProviderTest.kt @@ -0,0 +1,27 @@ +package co.electriccoin.zcash.configuration.internal.intent + +import android.content.Intent +import androidx.test.filters.SmallTest +import co.electriccoin.zcash.configuration.model.entry.ConfigKey +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Test + +class IntentProviderTest { + @Test + @SmallTest + fun testInsertValue() { + val key = ConfigKey("test") + assertFalse(IntentConfigurationProvider.peekConfiguration().hasKey(key)) + + IntentConfigurationReceiver().onReceive( + null, + Intent().apply { + putExtra(ConfigurationIntent.EXTRA_STRING_KEY, key.key) + putExtra(ConfigurationIntent.EXTRA_STRING_VALUE, "test") + } + ) + + assertTrue(IntentConfigurationProvider.peekConfiguration().hasKey(key)) + } +} diff --git a/configuration-impl-android-lib/src/debug/AndroidManifest.xml b/configuration-impl-android-lib/src/debug/AndroidManifest.xml new file mode 100644 index 00000000..785b235f --- /dev/null +++ b/configuration-impl-android-lib/src/debug/AndroidManifest.xml @@ -0,0 +1,12 @@ + + + + + + + + + diff --git a/configuration-impl-android-lib/src/main/AndroidManifest.xml b/configuration-impl-android-lib/src/main/AndroidManifest.xml new file mode 100644 index 00000000..b03c811c --- /dev/null +++ b/configuration-impl-android-lib/src/main/AndroidManifest.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/configuration-impl-android-lib/src/main/java/co/electriccoin/zcash/configuration/AndroidConfigurationFactory.kt b/configuration-impl-android-lib/src/main/java/co/electriccoin/zcash/configuration/AndroidConfigurationFactory.kt new file mode 100644 index 00000000..14f1c458 --- /dev/null +++ b/configuration-impl-android-lib/src/main/java/co/electriccoin/zcash/configuration/AndroidConfigurationFactory.kt @@ -0,0 +1,33 @@ +package co.electriccoin.zcash.configuration + +import android.content.Context +import co.electriccoin.zcash.configuration.api.ConfigurationProvider +import co.electriccoin.zcash.configuration.api.MergingConfigurationProvider +import co.electriccoin.zcash.configuration.internal.intent.IntentConfigurationProvider +import co.electriccoin.zcash.spackle.LazyWithArgument +import kotlinx.collections.immutable.toPersistentList + +object AndroidConfigurationFactory { + + private val instance = LazyWithArgument { context -> + new(context) + } + + fun getInstance(context: Context): ConfigurationProvider = instance.getInstance(context) + + // Context will be needed for most cloud providers, e.g. to integrate with Firebase or other + // remote configuration providers. + private fun new(@Suppress("UNUSED_PARAMETER") context: Context): ConfigurationProvider { + val configurationProviders = buildList { + // For ordering, ensure the IntentConfigurationProvider is first so that it can + // override any other configuration providers. + if (BuildConfig.DEBUG) { + add(IntentConfigurationProvider) + } + + // In the future, add a third party cloud-based configuration provider + } + + return MergingConfigurationProvider(configurationProviders.toPersistentList()) + } +} diff --git a/configuration-impl-android-lib/src/main/java/co/electriccoin/zcash/configuration/internal/intent/ConfigurationIntent.kt b/configuration-impl-android-lib/src/main/java/co/electriccoin/zcash/configuration/internal/intent/ConfigurationIntent.kt new file mode 100644 index 00000000..c4dfffab --- /dev/null +++ b/configuration-impl-android-lib/src/main/java/co/electriccoin/zcash/configuration/internal/intent/ConfigurationIntent.kt @@ -0,0 +1,7 @@ +package co.electriccoin.zcash.configuration.internal.intent + +internal object ConfigurationIntent { + const val EXTRA_STRING_KEY = "key" // $NON-NLS + + const val EXTRA_STRING_VALUE = "value" // $NON-NLS +} diff --git a/configuration-impl-android-lib/src/main/java/co/electriccoin/zcash/configuration/internal/intent/IntentConfigurationProvider.kt b/configuration-impl-android-lib/src/main/java/co/electriccoin/zcash/configuration/internal/intent/IntentConfigurationProvider.kt new file mode 100644 index 00000000..1ffa9f5b --- /dev/null +++ b/configuration-impl-android-lib/src/main/java/co/electriccoin/zcash/configuration/internal/intent/IntentConfigurationProvider.kt @@ -0,0 +1,30 @@ +package co.electriccoin.zcash.configuration.internal.intent + +import co.electriccoin.zcash.configuration.api.ConfigurationProvider +import co.electriccoin.zcash.configuration.model.map.Configuration +import co.electriccoin.zcash.configuration.model.map.StringConfiguration +import kotlinx.collections.immutable.persistentMapOf +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow + +internal object IntentConfigurationProvider : ConfigurationProvider { + + private val configurationStateFlow = MutableStateFlow(StringConfiguration(persistentMapOf(), null)) + + override fun peekConfiguration() = configurationStateFlow.value + + override fun getConfigurationFlow(): Flow = configurationStateFlow + + override fun hintToRefresh() { + // Do nothing + } + + /** + * Sets the configuration to the provided value. + * + * @see IntentConfigurationProvider + */ + internal fun setConfiguration(configuration: StringConfiguration) { + configurationStateFlow.value = configuration + } +} diff --git a/configuration-impl-android-lib/src/main/java/co/electriccoin/zcash/configuration/internal/intent/IntentConfigurationReceiver.kt b/configuration-impl-android-lib/src/main/java/co/electriccoin/zcash/configuration/internal/intent/IntentConfigurationReceiver.kt new file mode 100644 index 00000000..346a5c4e --- /dev/null +++ b/configuration-impl-android-lib/src/main/java/co/electriccoin/zcash/configuration/internal/intent/IntentConfigurationReceiver.kt @@ -0,0 +1,38 @@ +package co.electriccoin.zcash.configuration.internal.intent + +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import co.electriccoin.zcash.configuration.model.map.StringConfiguration +import kotlinx.collections.immutable.toPersistentMap +import kotlinx.datetime.Clock + +class IntentConfigurationReceiver : BroadcastReceiver() { + override fun onReceive(context: Context?, intent: Intent?) { + intent?.defuse()?.let { + val key = it.getStringExtra(ConfigurationIntent.EXTRA_STRING_KEY) + val value = it.getStringExtra(ConfigurationIntent.EXTRA_STRING_VALUE) + + if (null != key) { + val existingConfiguration = IntentConfigurationProvider.peekConfiguration().configurationMapping + val newConfiguration = if (null == value) { + existingConfiguration.remove(key) + } else { + existingConfiguration + (key to value) + } + + IntentConfigurationProvider.setConfiguration(StringConfiguration(newConfiguration.toPersistentMap(), Clock.System.now())) + } + } + } +} + +// https://issuetracker.google.com/issues/36927401 +private fun Intent.defuse(): Intent? { + return try { + extras?.containsKey(null) + this + } catch (@Suppress("SwallowedException", "TooGenericExceptionCaught") e: Exception) { + null + } +} diff --git a/docs/Architecture.md b/docs/Architecture.md index 21aada9a..49fd3a3a 100644 --- a/docs/Architecture.md +++ b/docs/Architecture.md @@ -3,10 +3,10 @@ _Note: This document will continue to be updated as the app is implemented._ # Gradle * Versions are declared in [gradle.properties](../gradle.properties). There's still enough inconsistency in how versions are handled in Gradle, that this is as close as we can get to a universal system. A version catalog is used for dependencies and is configured in [settings.gradle.kts](../settings.gradle.kts), but other versions like Gradle Plug-ins, the NDK version, Java version, and Android SDK versions don't fit into the version catalog model and are read directly from the properties - * Much of the Gradle configuration lives in [build-convention](../build-convention/) to prevent repetitive configuration as additional modules are added to the project + * Much of the Gradle configuration lives in [build-conventions-secant](../build-conventions-secant/) to prevent repetitive configuration as additional modules are added to the project * Build scripts are written in Kotlin, so that a single language is used across build and the app code bases * Only Gradle, Google, and JetBrains plug-ins are included in the critical path. Third party plug-ins can be used, but they're outside the critical path. For example, the Gradle Versions Plugin could be removed and wouldn't negatively impact local building, testing, or releasing the app - * Repository restrictions are enabled in [build-convention](../build-convention/settings.gradle.kts), [settings.gradle.kts](../settings.gradle.kts), and [build.gradle.kts](../build.gradle.kts) to reduce likelihood of pulling in an incorrect dependency. If adding a new dependency, these restrictions may need to be changed otherwise an error that the dependency cannot be found will be displayed + * Repository restrictions are enabled in [build-conventions-secant](../build-conventions-secant/settings.gradle.kts), [settings.gradle.kts](../settings.gradle.kts), and [build.gradle.kts](../build.gradle.kts) to reduce likelihood of pulling in an incorrect dependency. If adding a new dependency, these restrictions may need to be changed otherwise an error that the dependency cannot be found will be displayed # Multiplatform While this repository is for an Android application, efforts are made to give multiplatform flexibility in the future. Specific adaptions that are being made: @@ -26,7 +26,9 @@ The logical components of the app are implemented as a number of Gradle modules. * `app` — Compiles all the modules together into the final application. This module contains minimal actual code. Note that the Java package structure for this module is under `co.electriccoin.zcash.app` while the Android package name is `co.electriccoin.zcash`. * `build-info-lib` — Collects information from the build environment (e.g. Git SHA, Git commit count) and compiles them into the application. Can also be used for injection of API keys or other secrets. - * `configuration-api-lib` — Multiplatform interfaces for remote configuration. + * configuration + * `configuration-api-lib` — Multiplatform interfaces for remote configuration. + * `configuration-impl-android-lib` — Android-specific implementation for remote configuration storage. * crash — For collecting and reporting exceptions and crashes * `crash-lib` — Common crash collection logic for Kotlin and JVM. This is not fully-featured by itself, but the long-term plan is multiplatform support. * `crash-android-lib` — Android-specific crash collection logic, built on top of the common and JVM implementation in `crash-lib` @@ -57,6 +59,11 @@ The following diagram shows a rough depiction of dependencies between the module sdkExtLib[[sdk-ext-lib]]; end sdkLib[[sdk-lib]] --> sdkExtLib[[sdk-ext-lib]]; + subgraph configuration + configurationApiLib[[configuration-api-lib]]; + configurationImplAndroidLib[[configuration-impl-android-lib]]; + end + configurationApiLib[[configuration-api-lib]] --> configurationImplAndroidLib[[configuration-impl-android-lib]]; subgraph preference preferenceApiLib[[preference-api-lib]]; preferenceImplAndroidLib[[preference-impl-android-lib]]; @@ -82,6 +89,7 @@ The following diagram shows a rough depiction of dependencies between the module spackleAndroidLib[[spackle-android-lib]]; end spackleLib[[spackle-lib]] --> spackleAndroidLib[[spackle-android-lib]]; + configuration --> ui[[ui]]; preference --> ui[[ui]]; sdk --> ui[[ui]]; spackle[[spackle]] --> ui[[ui]]; @@ -93,6 +101,15 @@ The following diagram shows a rough depiction of dependencies between the module # Test Fixtures Until the Kotlin adopts support for fixtures, fixtures live within the main source modules. These fixtures make it easy to write automated tests, as well as create Compose previews. Although these fixtures are compiled into the main application, they should be removed by R8 in release builds. +# Debugging +The application has support for remote configuration (aka feature toggles), which allows decoupling of releases from features being enabled. + +Debug builds allow for manual override of feature toggle entries, which can be set by command line invocations. These overrides last for the lifetime of the process, so they will reset if the process dies. Pressing the home button on Android does not necessarily stop the process, so the best way to ensure process death is to choose Force Stop in the Android settings. + +To set a configuration value manually, run the following shell command replacing `$SOME_KEY` and `$SOME_VALUE` with the key-value pair you'd like to set. The change will take effect immediately. + +`adb shell am broadcast -n co.electriccoin.zcash/co.electriccoin.zcash.configuration.internal.intent.IntentConfigurationReceiver --es key "$SOME_KEY" --es value "$NEW_VALUE"` + # Shared Resources There are some app-wide resources that share a common namespace, and these should be documented here to make it easy to ensure there are no collisions. diff --git a/docs/testing/manual_testing/Release build.md b/docs/testing/manual_testing/Release build.md new file mode 100644 index 00000000..f7da324f --- /dev/null +++ b/docs/testing/manual_testing/Release build.md @@ -0,0 +1,11 @@ +The app has both debug and release builds, as well as testnet and mainnet flavors. We deploy release mainnet builds to users, but often use debug mainnet or testnet builds for testing. + +# Ensure remote config debugging is disabled +1. Download the release APK from CI server or from Google Play (you can also build it locally with `./gradlew assembleRelease` but fetching the version from CI or Google Play will be closer to the version users receive) +1. In Android Studio, go to the Build menu and choose Analyze APK +1. Navigate to the APK file +1. Inspect the AndroidManifest and ensure that no `` entry for `IntentConfigurationReceiver` exists + +# Ensure logging is stripped + +# Ensure application is minified - The application is minified through R8 without obfuscation. This is especially important to improve runtime performance of the app that we release. diff --git a/settings.gradle.kts b/settings.gradle.kts index 1dd0e29b..961d1c4a 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -310,6 +310,7 @@ includeBuild("build-conventions-secant") include("app") include("build-info-lib") include("configuration-api-lib") +include("configuration-impl-android-lib") include("crash-lib") include("crash-android-lib") include("preference-api-lib") diff --git a/ui-lib/build.gradle.kts b/ui-lib/build.gradle.kts index acf8cfb8..b062fd03 100644 --- a/ui-lib/build.gradle.kts +++ b/ui-lib/build.gradle.kts @@ -73,6 +73,8 @@ dependencies { implementation(libs.zxing) implementation(projects.buildInfoLib) + implementation(projects.configurationApiLib) + implementation(projects.configurationImplAndroidLib) implementation(projects.crashAndroidLib) implementation(projects.preferenceApiLib) implementation(projects.preferenceImplAndroidLib) diff --git a/ui-lib/src/androidTest/java/co/electriccoin/zcash/ui/configuration/ConfigurationEntriesTest.kt b/ui-lib/src/androidTest/java/co/electriccoin/zcash/ui/configuration/ConfigurationEntriesTest.kt new file mode 100644 index 00000000..e33a0612 --- /dev/null +++ b/ui-lib/src/androidTest/java/co/electriccoin/zcash/ui/configuration/ConfigurationEntriesTest.kt @@ -0,0 +1,26 @@ +package co.electriccoin.zcash.ui.configuration + +import androidx.test.filters.SmallTest +import co.electriccoin.zcash.configuration.model.entry.DefaultEntry +import org.junit.Test +import kotlin.reflect.full.memberProperties +import kotlin.test.assertFalse + +class ConfigurationEntriesTest { + // This test is primary to prevent copy-paste errors in configuration keys + @SmallTest + @Test + fun keys_unique() { + val fieldValueSet = mutableSetOf() + + ConfigurationEntries::class.memberProperties + .map { it.getter.call(ConfigurationEntries) } + .map { it as DefaultEntry<*> } + .map { it.key } + .forEach { + assertFalse(fieldValueSet.contains(it.key), "Duplicate key $it") + + fieldValueSet.add(it.key) + } + } +} diff --git a/ui-lib/src/androidTest/java/co/electriccoin/zcash/ui/preference/EncryptedPreferenceKeysTest.kt b/ui-lib/src/androidTest/java/co/electriccoin/zcash/ui/preference/EncryptedPreferenceKeysTest.kt index 91433120..be7c5403 100644 --- a/ui-lib/src/androidTest/java/co/electriccoin/zcash/ui/preference/EncryptedPreferenceKeysTest.kt +++ b/ui-lib/src/androidTest/java/co/electriccoin/zcash/ui/preference/EncryptedPreferenceKeysTest.kt @@ -1,24 +1,24 @@ package co.electriccoin.zcash.ui.preference import androidx.test.filters.SmallTest -import org.hamcrest.CoreMatchers.equalTo -import org.hamcrest.MatcherAssert.assertThat +import co.electriccoin.zcash.preference.model.entry.PreferenceDefault import org.junit.Test import kotlin.reflect.full.memberProperties +import kotlin.test.assertFalse class EncryptedPreferenceKeysTest { // This test is primary to prevent copy-paste errors in preference keys @SmallTest @Test - fun key_values_unique() { + fun unique_keys() { val fieldValueSet = mutableSetOf() EncryptedPreferenceKeys::class.memberProperties .map { it.getter.call(EncryptedPreferenceKeys) } - .map { it as PersistableWalletPreferenceDefault } + .map { it as PreferenceDefault<*> } .map { it.key } .forEach { - assertThat("Duplicate key $it", fieldValueSet.contains(it.key), equalTo(false)) + assertFalse(fieldValueSet.contains(it.key), "Duplicate key $it") fieldValueSet.add(it.key) } diff --git a/ui-lib/src/androidTest/java/co/electriccoin/zcash/ui/preference/StandardPreferenceKeysTest.kt b/ui-lib/src/androidTest/java/co/electriccoin/zcash/ui/preference/StandardPreferenceKeysTest.kt new file mode 100644 index 00000000..b1467c3c --- /dev/null +++ b/ui-lib/src/androidTest/java/co/electriccoin/zcash/ui/preference/StandardPreferenceKeysTest.kt @@ -0,0 +1,26 @@ +package co.electriccoin.zcash.ui.preference + +import androidx.test.filters.SmallTest +import co.electriccoin.zcash.preference.model.entry.PreferenceDefault +import org.junit.Test +import kotlin.reflect.full.memberProperties +import kotlin.test.assertFalse + +class StandardPreferenceKeysTest { + // This test is primary to prevent copy-paste errors in preference keys + @SmallTest + @Test + fun unique_keys() { + val fieldValueSet = mutableSetOf() + + StandardPreferenceKeys::class.memberProperties + .map { it.getter.call(StandardPreferenceKeys) } + .map { it as PreferenceDefault<*> } + .map { it.key } + .forEach { + assertFalse(fieldValueSet.contains(it.key), "Duplicate key $it") + + fieldValueSet.add(it.key) + } + } +} diff --git a/ui-lib/src/androidTest/java/co/electriccoin/zcash/ui/screen/about/AboutViewTest.kt b/ui-lib/src/androidTest/java/co/electriccoin/zcash/ui/screen/about/AboutViewTest.kt index 3ce7a7b0..31fa134b 100644 --- a/ui-lib/src/androidTest/java/co/electriccoin/zcash/ui/screen/about/AboutViewTest.kt +++ b/ui-lib/src/androidTest/java/co/electriccoin/zcash/ui/screen/about/AboutViewTest.kt @@ -9,9 +9,11 @@ import androidx.test.filters.MediumTest import co.electriccoin.zcash.build.gitSha import co.electriccoin.zcash.ui.R import co.electriccoin.zcash.ui.design.theme.ZcashTheme +import co.electriccoin.zcash.ui.fixture.ConfigInfoFixture import co.electriccoin.zcash.ui.fixture.VersionInfoFixture import co.electriccoin.zcash.ui.screen.about.model.VersionInfo import co.electriccoin.zcash.ui.screen.about.view.About +import co.electriccoin.zcash.ui.screen.support.model.ConfigInfo import co.electriccoin.zcash.ui.test.getStringResource import org.junit.Assert.assertEquals import org.junit.Rule @@ -50,9 +52,17 @@ class AboutViewTest { assertEquals(1, testSetup.getOnBackCount()) } - private fun newTestSetup() = TestSetup(composeTestRule, VersionInfoFixture.new()) + private fun newTestSetup() = TestSetup( + composeTestRule, + VersionInfoFixture.new(), + ConfigInfoFixture.new() + ) - private class TestSetup(private val composeTestRule: ComposeContentTestRule, versionInfo: VersionInfo) { + private class TestSetup( + private val composeTestRule: ComposeContentTestRule, + versionInfo: VersionInfo, + configInfo: ConfigInfo + ) { private val onBackCount = AtomicInteger(0) @@ -64,7 +74,7 @@ class AboutViewTest { init { composeTestRule.setContent { ZcashTheme { - About(versionInfo = versionInfo) { + About(versionInfo = versionInfo, configInfo = configInfo) { onBackCount.incrementAndGet() } } diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/ui/MainActivity.kt b/ui-lib/src/main/java/co/electriccoin/zcash/ui/MainActivity.kt index f6d2ba16..8b35a96e 100644 --- a/ui-lib/src/main/java/co/electriccoin/zcash/ui/MainActivity.kt +++ b/ui-lib/src/main/java/co/electriccoin/zcash/ui/MainActivity.kt @@ -9,6 +9,7 @@ import androidx.annotation.VisibleForTesting import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen @@ -18,6 +19,7 @@ import androidx.lifecycle.lifecycleScope import androidx.lifecycle.repeatOnLifecycle import androidx.navigation.NavHostController import co.electriccoin.zcash.ui.common.BindCompLocalProvider +import co.electriccoin.zcash.ui.configuration.RemoteConfig import co.electriccoin.zcash.ui.design.component.ConfigurationOverride import co.electriccoin.zcash.ui.design.component.GradientSurface import co.electriccoin.zcash.ui.design.component.Override @@ -41,6 +43,8 @@ import kotlin.time.Duration.Companion.seconds class MainActivity : ComponentActivity() { + val homeViewModel by viewModels() + val walletViewModel by viewModels() @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) @@ -74,7 +78,8 @@ class MainActivity : ComponentActivity() { } } - SecretState.Loading == walletViewModel.secretState.value + // Note this condition needs to be kept in sync with the condition in MainContent() + homeViewModel.configurationFlow.value == null || SecretState.Loading == walletViewModel.secretState.value } } @@ -107,22 +112,35 @@ class MainActivity : ComponentActivity() { @Composable private fun MainContent() { - when (val secretState = walletViewModel.secretState.collectAsStateWithLifecycle().value) { - SecretState.Loading -> { - // For now, keep displaying splash screen using condition above. - // In the future, we might consider displaying something different here. - } - SecretState.None -> { - WrapOnboarding() - } - is SecretState.NeedsBackup -> { - WrapBackup( - secretState.persistableWallet, - onBackupComplete = { walletViewModel.persistBackupComplete() } - ) - } - is SecretState.Ready -> { - Navigation() + val configuration = homeViewModel.configurationFlow.collectAsStateWithLifecycle().value + val secretState = walletViewModel.secretState.collectAsStateWithLifecycle().value + + // Note this condition needs to be kept in sync with the condition in setupSplashScreen() + if (null == configuration || secretState == SecretState.Loading) { + // For now, keep displaying splash screen using condition above. + // In the future, we might consider displaying something different here. + } else { + // Note that the deeply nested child views will probably receive arguments derived from + // the configuration. The CompositionLocalProvider is helpful for passing the configuration + // to the "platform" layer, which is where the arguments will be derived from. + CompositionLocalProvider(RemoteConfig provides configuration) { + when (secretState) { + SecretState.None -> { + WrapOnboarding() + } + is SecretState.NeedsBackup -> { + WrapBackup( + secretState.persistableWallet, + onBackupComplete = { walletViewModel.persistBackupComplete() } + ) + } + is SecretState.Ready -> { + Navigation() + } + else -> { + error("Unhandled secret state: $secretState") + } + } } } } diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/ui/Navigation.kt b/ui-lib/src/main/java/co/electriccoin/zcash/ui/Navigation.kt index 41f9144c..6b697502 100644 --- a/ui-lib/src/main/java/co/electriccoin/zcash/ui/Navigation.kt +++ b/ui-lib/src/main/java/co/electriccoin/zcash/ui/Navigation.kt @@ -17,6 +17,8 @@ import co.electriccoin.zcash.ui.NavigationTargets.SEND import co.electriccoin.zcash.ui.NavigationTargets.SETTINGS import co.electriccoin.zcash.ui.NavigationTargets.SUPPORT import co.electriccoin.zcash.ui.NavigationTargets.WALLET_ADDRESS_DETAILS +import co.electriccoin.zcash.ui.configuration.ConfigurationEntries +import co.electriccoin.zcash.ui.configuration.RemoteConfig import co.electriccoin.zcash.ui.screen.about.WrapAbout import co.electriccoin.zcash.ui.screen.address.WrapWalletAddresses import co.electriccoin.zcash.ui.screen.home.WrapHome @@ -47,7 +49,9 @@ internal fun MainActivity.Navigation() { goRequest = { navController.navigateJustOnce(REQUEST) } ) - WrapCheckForUpdate() + if (ConfigurationEntries.IS_APP_UPDATE_CHECK_ENABLED.getValue(RemoteConfig.current)) { + WrapCheckForUpdate() + } } composable(PROFILE) { WrapProfile( diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/ui/configuration/ConfigurationEntries.kt b/ui-lib/src/main/java/co/electriccoin/zcash/ui/configuration/ConfigurationEntries.kt new file mode 100644 index 00000000..6161364a --- /dev/null +++ b/ui-lib/src/main/java/co/electriccoin/zcash/ui/configuration/ConfigurationEntries.kt @@ -0,0 +1,14 @@ +package co.electriccoin.zcash.ui.configuration + +import co.electriccoin.zcash.configuration.model.entry.BooleanConfigurationEntry +import co.electriccoin.zcash.configuration.model.entry.ConfigKey + +object ConfigurationEntries { + val IS_APP_UPDATE_CHECK_ENABLED = BooleanConfigurationEntry(ConfigKey("is_update_check_enabled"), true) + + /* + * Disabled because we don't have the URI parser support in the SDK yet. + * + */ + val IS_REQUEST_ZEC_ENABLED = BooleanConfigurationEntry(ConfigKey("is_update_check_enabled"), false) +} diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/ui/configuration/ConfigurationLocal.kt b/ui-lib/src/main/java/co/electriccoin/zcash/ui/configuration/ConfigurationLocal.kt new file mode 100644 index 00000000..83aca34a --- /dev/null +++ b/ui-lib/src/main/java/co/electriccoin/zcash/ui/configuration/ConfigurationLocal.kt @@ -0,0 +1,8 @@ +package co.electriccoin.zcash.ui.configuration + +import androidx.compose.runtime.compositionLocalOf +import co.electriccoin.zcash.configuration.model.map.Configuration +import co.electriccoin.zcash.configuration.model.map.StringConfiguration +import kotlinx.collections.immutable.persistentMapOf + +val RemoteConfig = compositionLocalOf { StringConfiguration(persistentMapOf(), null) } diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/ui/fixture/ConfigInfoFixture.kt b/ui-lib/src/main/java/co/electriccoin/zcash/ui/fixture/ConfigInfoFixture.kt new file mode 100644 index 00000000..90c52e3e --- /dev/null +++ b/ui-lib/src/main/java/co/electriccoin/zcash/ui/fixture/ConfigInfoFixture.kt @@ -0,0 +1,15 @@ +package co.electriccoin.zcash.ui.fixture + +import co.electriccoin.zcash.ui.screen.support.model.ConfigInfo +import kotlinx.datetime.Instant +import kotlinx.datetime.toInstant + +// Magic Number doesn't matter here for hard-coded fixture values +@Suppress("MagicNumber") +object ConfigInfoFixture { + val UPDATED_AT = "2023-01-15T08:38:45.415Z".toInstant() + + fun new( + updatedAt: Instant? = UPDATED_AT, + ) = ConfigInfo(updatedAt) +} diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/about/AndroidAboutView.kt b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/about/AndroidAboutView.kt index d0e6dc5c..e370687b 100644 --- a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/about/AndroidAboutView.kt +++ b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/about/AndroidAboutView.kt @@ -4,10 +4,13 @@ package co.electriccoin.zcash.ui.screen.about import androidx.activity.ComponentActivity import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import co.electriccoin.zcash.configuration.AndroidConfigurationFactory import co.electriccoin.zcash.spackle.getPackageInfoCompat import co.electriccoin.zcash.ui.MainActivity import co.electriccoin.zcash.ui.screen.about.model.VersionInfo import co.electriccoin.zcash.ui.screen.about.view.About +import co.electriccoin.zcash.ui.screen.support.model.ConfigInfo @Composable internal fun MainActivity.WrapAbout( @@ -22,6 +25,12 @@ internal fun WrapAbout( goBack: () -> Unit ) { val packageInfo = activity.packageManager.getPackageInfoCompat(activity.packageName, 0L) + val configurationProvider = AndroidConfigurationFactory.getInstance(activity.applicationContext) - About(VersionInfo.new(packageInfo), goBack) + About(VersionInfo.new(packageInfo), ConfigInfo.new(configurationProvider), goBack) + + // Allows an implicit way to force configuration refresh by simply visiting the About screen + LaunchedEffect(key1 = true) { + AndroidConfigurationFactory.getInstance(activity.applicationContext).hintToRefresh() + } } diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/about/view/AboutView.kt b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/about/view/AboutView.kt index dd76a234..da41b4b1 100644 --- a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/about/view/AboutView.kt +++ b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/about/view/AboutView.kt @@ -27,15 +27,21 @@ import co.electriccoin.zcash.ui.design.component.Body import co.electriccoin.zcash.ui.design.component.GradientSurface import co.electriccoin.zcash.ui.design.component.Header import co.electriccoin.zcash.ui.design.theme.ZcashTheme +import co.electriccoin.zcash.ui.fixture.ConfigInfoFixture import co.electriccoin.zcash.ui.fixture.VersionInfoFixture import co.electriccoin.zcash.ui.screen.about.model.VersionInfo +import co.electriccoin.zcash.ui.screen.support.model.ConfigInfo @Preview @Composable fun AboutPreview() { ZcashTheme(darkTheme = true) { GradientSurface { - About(versionInfo = VersionInfoFixture.new(), goBack = {}) + About( + versionInfo = VersionInfoFixture.new(), + configInfo = ConfigInfoFixture.new(), + goBack = {} + ) } } } @@ -44,6 +50,7 @@ fun AboutPreview() { @Composable fun About( versionInfo: VersionInfo, + configInfo: ConfigInfo, goBack: () -> Unit ) { Scaffold(topBar = { @@ -51,7 +58,8 @@ fun About( }) { paddingValues -> AboutMainContent( paddingValues, - versionInfo + versionInfo, + configInfo ) } } @@ -75,7 +83,7 @@ private fun AboutTopAppBar(onBack: () -> Unit) { } @Composable -fun AboutMainContent(paddingValues: PaddingValues, versionInfo: VersionInfo) { +fun AboutMainContent(paddingValues: PaddingValues, versionInfo: VersionInfo, configInfo: ConfigInfo) { Column( Modifier .verticalScroll(rememberScrollState()) @@ -96,6 +104,13 @@ fun AboutMainContent(paddingValues: PaddingValues, versionInfo: VersionInfo) { Spacer(modifier = Modifier.height(24.dp)) + configInfo.configurationUpdatedAt?.let { updatedAt -> + Header(stringResource(id = R.string.about_build_configuration)) + Body(updatedAt.toString()) + } + + Spacer(modifier = Modifier.height(24.dp)) + Header(stringResource(id = R.string.about_legal_header)) Body(stringResource(id = R.string.about_legal_info)) } diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/home/viewmodel/HomeViewModel.kt b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/home/viewmodel/HomeViewModel.kt index 8539abf3..3cec1c2a 100644 --- a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/home/viewmodel/HomeViewModel.kt +++ b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/home/viewmodel/HomeViewModel.kt @@ -3,6 +3,8 @@ package co.electriccoin.zcash.ui.screen.home.viewmodel import android.app.Application import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.viewModelScope +import co.electriccoin.zcash.configuration.AndroidConfigurationFactory +import co.electriccoin.zcash.configuration.model.map.Configuration import co.electriccoin.zcash.ui.common.ANDROID_STATE_FLOW_TIMEOUT import co.electriccoin.zcash.ui.preference.StandardPreferenceKeys import co.electriccoin.zcash.ui.preference.StandardPreferenceSingleton @@ -21,4 +23,7 @@ class HomeViewModel(application: Application) : AndroidViewModel(application) { val preferenceProvider = StandardPreferenceSingleton.getInstance(application) emitAll(StandardPreferenceKeys.IS_BACKGROUND_SYNC_ENABLED.observe(preferenceProvider)) }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(ANDROID_STATE_FLOW_TIMEOUT.inWholeMilliseconds), null) + + val configurationFlow: StateFlow = AndroidConfigurationFactory.getInstance(application).getConfigurationFlow() + .stateIn(viewModelScope, SharingStarted.WhileSubscribed(ANDROID_STATE_FLOW_TIMEOUT.inWholeMilliseconds), null) } diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/support/model/AppInfo.kt b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/support/model/AppInfo.kt index 6acbae54..cdb6c367 100644 --- a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/support/model/AppInfo.kt +++ b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/support/model/AppInfo.kt @@ -4,7 +4,7 @@ import android.content.pm.PackageInfo import co.electriccoin.zcash.build.gitSha import co.electriccoin.zcash.spackle.versionCodeCompat -class AppInfo(val versionName: String, val versionCode: Long, val gitSha: String) { +data class AppInfo(val versionName: String, val versionCode: Long, val gitSha: String) { fun toSupportString() = buildString { appendLine("App version: $versionName ($versionCode) $gitSha") diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/support/model/ConfigInfo.kt b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/support/model/ConfigInfo.kt new file mode 100644 index 00000000..f78de3ab --- /dev/null +++ b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/support/model/ConfigInfo.kt @@ -0,0 +1,17 @@ +package co.electriccoin.zcash.ui.screen.support.model + +import co.electriccoin.zcash.configuration.api.ConfigurationProvider +import kotlinx.datetime.Instant + +data class ConfigInfo(val configurationUpdatedAt: Instant?) { + + fun toSupportString() = buildString { + appendLine("Configuration: $configurationUpdatedAt") + } + + companion object { + fun new(configurationProvider: ConfigurationProvider) = ConfigInfo( + configurationProvider.peekConfiguration().updatedAt + ) + } +} diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/support/model/CrashInfo.kt b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/support/model/CrashInfo.kt index 52205172..87e35a9a 100644 --- a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/support/model/CrashInfo.kt +++ b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/support/model/CrashInfo.kt @@ -9,7 +9,7 @@ import co.electriccoin.zcash.spackle.io.listFilesSuspend import kotlinx.datetime.Instant import java.io.File -class CrashInfo(val exceptionClassName: String, val isUncaught: Boolean, val timestamp: Instant) { +data class CrashInfo(val exceptionClassName: String, val isUncaught: Boolean, val timestamp: Instant) { fun toSupportString() = buildString { appendLine("Exception") appendLine(" Class name: $exceptionClassName") diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/support/model/DeviceInfo.kt b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/support/model/DeviceInfo.kt index 8d33e3b6..dc9fa3e2 100644 --- a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/support/model/DeviceInfo.kt +++ b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/support/model/DeviceInfo.kt @@ -2,7 +2,7 @@ package co.electriccoin.zcash.ui.screen.support.model import android.os.Build -class DeviceInfo(val manufacturer: String, val device: String, val model: String) { +data class DeviceInfo(val manufacturer: String, val device: String, val model: String) { fun toSupportString() = buildString { appendLine("Device: $manufacturer $device $model") diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/support/model/EnvironmentInfo.kt b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/support/model/EnvironmentInfo.kt index 6aab7160..d94eeda6 100644 --- a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/support/model/EnvironmentInfo.kt +++ b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/support/model/EnvironmentInfo.kt @@ -5,7 +5,7 @@ import cash.z.ecc.android.sdk.model.MonetarySeparators import co.electriccoin.zcash.global.StorageChecker import java.util.Locale -class EnvironmentInfo(val locale: Locale, val monetarySeparators: MonetarySeparators, val usableStorageMegabytes: Int) { +data class EnvironmentInfo(val locale: Locale, val monetarySeparators: MonetarySeparators, val usableStorageMegabytes: Int) { fun toSupportString() = buildString { appendLine("Locale: ${locale.androidResName()}") diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/support/model/OperatingSystemInfo.kt b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/support/model/OperatingSystemInfo.kt index f9ecacd3..70f14635 100644 --- a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/support/model/OperatingSystemInfo.kt +++ b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/support/model/OperatingSystemInfo.kt @@ -3,7 +3,7 @@ package co.electriccoin.zcash.ui.screen.support.model import android.os.Build import co.electriccoin.zcash.spackle.AndroidApiVersion -class OperatingSystemInfo(val sdkInt: Int, val isPreview: Boolean) { +data class OperatingSystemInfo(val sdkInt: Int, val isPreview: Boolean) { fun toSupportString() = buildString { if (isPreview) { diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/support/model/PermissionInfo.kt b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/support/model/PermissionInfo.kt index 6ede30ae..b044df22 100644 --- a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/support/model/PermissionInfo.kt +++ b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/support/model/PermissionInfo.kt @@ -6,7 +6,7 @@ import android.content.pm.PackageInfo import android.content.pm.PackageManager import co.electriccoin.zcash.spackle.getPackageInfoCompatSuspend -class PermissionInfo(val permissionName: String, val permissionStatus: PermissionStatus) { +data class PermissionInfo(val permissionName: String, val permissionStatus: PermissionStatus) { fun toSupportString() = buildString { appendLine("$permissionName $permissionStatus") } diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/support/model/SupportInfo.kt b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/support/model/SupportInfo.kt index 236a2d1e..ac92e543 100644 --- a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/support/model/SupportInfo.kt +++ b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/support/model/SupportInfo.kt @@ -1,7 +1,10 @@ package co.electriccoin.zcash.ui.screen.support.model import android.content.Context +import co.electriccoin.zcash.configuration.AndroidConfigurationFactory import co.electriccoin.zcash.spackle.getPackageInfoCompatSuspend +import kotlinx.collections.immutable.PersistentList +import kotlinx.collections.immutable.toPersistentList enum class SupportInfoType { Time, @@ -16,11 +19,12 @@ enum class SupportInfoType { data class SupportInfo( val timeInfo: TimeInfo, val appInfo: AppInfo, + val configInfo: ConfigInfo, val operatingSystemInfo: OperatingSystemInfo, val deviceInfo: DeviceInfo, val environmentInfo: EnvironmentInfo, - val permissionInfo: List, - val crashInfo: List + val permissionInfo: PersistentList, + val crashInfo: PersistentList ) { // The set of enum values is to allow optional filtering of different types of information @@ -62,15 +66,17 @@ data class SupportInfo( suspend fun new(context: Context): SupportInfo { val applicationContext = context.applicationContext val packageInfo = applicationContext.packageManager.getPackageInfoCompatSuspend(context.packageName, 0L) + val configurationProvider = AndroidConfigurationFactory.getInstance(applicationContext) return SupportInfo( TimeInfo.new(packageInfo), AppInfo.new(packageInfo), + ConfigInfo.new(configurationProvider), OperatingSystemInfo.new(), DeviceInfo.new(), EnvironmentInfo.new(applicationContext), - PermissionInfo.all(applicationContext), - CrashInfo.all(context) + PermissionInfo.all(applicationContext).toPersistentList(), + CrashInfo.all(context).toPersistentList() ) } } diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/support/model/TimeInfo.kt b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/support/model/TimeInfo.kt index 170db04f..cdf7b853 100644 --- a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/support/model/TimeInfo.kt +++ b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/support/model/TimeInfo.kt @@ -9,7 +9,7 @@ import java.util.Date import java.util.Locale import kotlin.time.Duration.Companion.milliseconds -class TimeInfo( +data class TimeInfo( val currentTime: Instant, val rebootTime: Instant, val installTime: Instant, diff --git a/ui-lib/src/main/res/ui/about/values/strings.xml b/ui-lib/src/main/res/ui/about/values/strings.xml index 338e292c..294ab4e3 100644 --- a/ui-lib/src/main/res/ui/about/values/strings.xml +++ b/ui-lib/src/main/res/ui/about/values/strings.xml @@ -5,6 +5,7 @@ Version %1$s (%2$d) Build + Configuration Legal