Dependency injection implementation (#1513)

* Dependency injection implementation

* Code cleanup

* Code cleanup

* Test hotfix

* Code cleanup

* Code cleanup

* Code cleanup

* Merge dependencies to bundle

* Changelog update

---------

Co-authored-by: Honza <rychnovsky.honza@gmail.com>
This commit is contained in:
Milan 2024-09-04 15:36:59 +02:00 committed by GitHub
parent 23603dcd29
commit d29b0f7bb2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
75 changed files with 627 additions and 445 deletions

View File

@ -6,6 +6,12 @@ and this application adheres to [Semantic Versioning](https://semver.org/spec/v2
## [Unreleased]
### Added
- Balance now also displays USD value
- An option to enter USD amount in Send Transaction screen
- Dependency injection using Koin has been added to the project. This helps us keep the codebase organized while
adding new app features.
## [1.1.6 (712)] - 2024-09-04
### Added

View File

@ -1,14 +1,24 @@
package co.electriccoin.zcash.app
import co.electriccoin.zcash.crash.android.GlobalCrashReporter
import co.electriccoin.zcash.di.coreModule
import co.electriccoin.zcash.di.repositoryModule
import co.electriccoin.zcash.di.useCaseModule
import co.electriccoin.zcash.di.viewModelModule
import co.electriccoin.zcash.preference.StandardPreferenceProvider
import co.electriccoin.zcash.spackle.StrictModeCompat
import co.electriccoin.zcash.spackle.Twig
import co.electriccoin.zcash.ui.preference.StandardPreferenceKeys
import co.electriccoin.zcash.ui.preference.StandardPreferenceSingleton
import kotlinx.coroutines.launch
import org.koin.android.ext.android.inject
import org.koin.android.ext.koin.androidContext
import org.koin.android.ext.koin.androidLogger
import org.koin.core.context.startKoin
@Suppress("unused")
class ZcashApplication : CoroutineApplication() {
private val standardPreferenceProvider by inject<StandardPreferenceProvider>()
override fun onCreate() {
super.onCreate()
@ -16,6 +26,17 @@ class ZcashApplication : CoroutineApplication() {
configureStrictMode()
startKoin {
androidLogger()
androidContext(this@ZcashApplication)
modules(
coreModule,
repositoryModule,
useCaseModule,
viewModelModule
)
}
// Since analytics will need disk IO internally, we want this to be registered after strict
// mode is configured to ensure none of that IO happens on the main thread
configureAnalytics()
@ -40,8 +61,7 @@ class ZcashApplication : CoroutineApplication() {
private fun configureAnalytics() {
if (GlobalCrashReporter.register(this)) {
applicationScope.launch {
val prefs = StandardPreferenceSingleton.getInstance(applicationContext)
StandardPreferenceKeys.IS_ANALYTICS_ENABLED.observe(prefs).collect {
StandardPreferenceKeys.IS_ANALYTICS_ENABLED.observe(standardPreferenceProvider()).collect {
if (it) {
GlobalCrashReporter.enable()
} else {

View File

@ -1,25 +1,12 @@
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, ConfigurationProvider> { context ->
new(context)
}
fun getInstance(context: Context): ConfigurationProvider = instance.getInstance(context)
// Context will be needed for most cloud providers, e.g. to integrate with Firebase or other
// remote configuration providers.
private fun new(
@Suppress("UNUSED_PARAMETER") context: Context
): ConfigurationProvider {
fun new(): ConfigurationProvider {
val configurationProviders =
buildList<ConfigurationProvider> {
// For ordering, ensure the IntentConfigurationProvider is first so that it can

View File

@ -117,8 +117,6 @@ As a specific example, the "Request ZEC" button on the home screen is currently
# 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.
* SharedPreferences
* "co.electriccoin.zcash.encrypted" is defined as a preference file in `EncryptedPreferenceSingleton.kt`
* Databases
* Some databases are defined by the SDK
* Notification IDs

View File

@ -155,6 +155,7 @@ GOOGLE_PLAY_SERVICES_GRADLE_PLUGIN_VERSION=4.4.1
GRADLE_VERSIONS_PLUGIN_VERSION=0.51.0
JGIT_VERSION=6.4.0.202211300538-r
KTLINT_VERSION=1.2.1
KOIN_VERSION=3.5.6
ACCOMPANIST_PERMISSIONS_VERSION=0.34.0
ANDROIDX_ACTIVITY_VERSION=1.8.2

View File

@ -28,12 +28,11 @@ import java.util.concurrent.Executors
* this instance lives for the lifetime of the application. Constructing multiple instances will
* potentially corrupt preference data and will leak resources.
*/
class AndroidPreferenceProvider(
class AndroidPreferenceProvider private constructor(
private val sharedPreferences: SharedPreferences,
private val dispatcher: CoroutineDispatcher
) : PreferenceProvider {
private val mutex = Mutex()
/*
* Implementation note: EncryptedSharedPreferences are not thread-safe, so this implementation
* confines them to a single background thread.
@ -76,7 +75,7 @@ class AndroidPreferenceProvider(
}
override fun observe(key: PreferenceKey): Flow<String?> =
callbackFlow<Unit> {
callbackFlow {
val listener =
SharedPreferences.OnSharedPreferenceChangeListener { _, _ ->
// Callback on main thread
@ -122,15 +121,13 @@ class AndroidPreferenceProvider(
*/
val singleThreadedDispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher()
val mainKey =
val sharedPreferences =
withContext(singleThreadedDispatcher) {
val mainKey =
MasterKey.Builder(context).apply {
setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
}.build()
}
val sharedPreferences =
withContext(singleThreadedDispatcher) {
EncryptedSharedPreferences.create(
context,
filename,

View File

@ -0,0 +1,10 @@
package co.electriccoin.zcash.preference
import android.content.Context
import co.electriccoin.zcash.preference.api.PreferenceProvider
class EncryptedPreferenceProvider(private val context: Context) : PreferenceHolder() {
override suspend fun create(): PreferenceProvider {
return AndroidPreferenceProvider.newEncrypted(context, "co.electriccoin.zcash.encrypted")
}
}

View File

@ -0,0 +1,26 @@
package co.electriccoin.zcash.preference
import co.electriccoin.zcash.preference.api.PreferenceProvider
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
abstract class PreferenceHolder {
private var preferenceProvider: PreferenceProvider? = null
private val mutex = Mutex()
suspend operator fun invoke(): PreferenceProvider =
mutex.withLock {
val preferenceProvider = preferenceProvider
if (preferenceProvider != null) {
return preferenceProvider
}
val newPreferenceProvider = create()
this.preferenceProvider = newPreferenceProvider
return newPreferenceProvider
}
protected abstract suspend fun create(): PreferenceProvider
}

View File

@ -0,0 +1,10 @@
package co.electriccoin.zcash.preference
import android.content.Context
import co.electriccoin.zcash.preference.api.PreferenceProvider
class StandardPreferenceProvider(private val context: Context) : PreferenceHolder() {
override suspend fun create(): PreferenceProvider {
return AndroidPreferenceProvider.newStandard(context, "co.electriccoin.zcash")
}
}

View File

@ -0,0 +1,5 @@
package cash.z.ecc.sdk
import kotlin.time.Duration.Companion.seconds
val ANDROID_STATE_FLOW_TIMEOUT = 5.seconds

View File

@ -183,6 +183,7 @@ dependencyResolutionManagement {
val zcashBip39Version = extra["ZCASH_BIP39_VERSION"].toString()
val zcashSdkVersion = extra["ZCASH_SDK_VERSION"].toString()
val zxingVersion = extra["ZXING_VERSION"].toString()
val koinVersion = extra["KOIN_VERSION"].toString()
// Standalone versions
version("flank", flankVersion)
@ -241,6 +242,8 @@ dependencyResolutionManagement {
library("zcash-sdk-incubator", "cash.z.ecc.android:zcash-android-sdk-incubator:$zcashSdkVersion")
library("zcash-bip39", "cash.z.ecc.android:kotlin-bip39:$zcashBip39Version")
library("zxing", "com.google.zxing:core:$zxingVersion")
library("koin", "io.insert-koin:koin-android:$koinVersion")
library("koin-compose", "io.insert-koin:koin-androidx-compose:$koinVersion")
// Test libraries
library("androidx-compose-test-junit", "androidx.compose.ui:ui-test-junit4:$androidxComposeVersion")
@ -294,13 +297,6 @@ dependencyResolutionManagement {
"androidx-viewmodel-compose"
)
)
bundle(
"play-update",
listOf(
"play-update",
"play-update-ktx",
)
)
bundle(
"androidx-test",
listOf(
@ -312,6 +308,20 @@ dependencyResolutionManagement {
"androidx-test-runner"
)
)
bundle(
"koin",
listOf(
"koin",
"koin-compose",
)
)
bundle(
"play-update",
listOf(
"play-update",
"play-update-ktx",
)
)
}
}
}

View File

@ -6,7 +6,7 @@ import android.os.PowerManager
import androidx.test.core.app.ApplicationProvider
import androidx.test.runner.AndroidJUnitRunner
class ZcashUiTestRunner : AndroidJUnitRunner() {
open class ZcashUiTestRunner : AndroidJUnitRunner() {
private lateinit var wakeLock: PowerManager.WakeLock
override fun onCreate(arguments: Bundle?) {

View File

@ -29,9 +29,8 @@ fun Override(
val contextWrapper =
run {
val context = LocalContext.current
object : ContextThemeWrapper() {
object : ContextThemeWrapper(context, null) {
init {
attachBaseContext(context)
applyOverrideConfiguration(configuration)
}
}

View File

@ -95,10 +95,11 @@ dependencies {
implementation(libs.androidx.lifecycle.livedata)
implementation(libs.androidx.splash)
implementation(libs.androidx.workmanager)
implementation(libs.bundles.androidx.biometric)
api(libs.bundles.androidx.biometric)
implementation(libs.bundles.androidx.camera)
implementation(libs.bundles.androidx.compose.core)
implementation(libs.bundles.androidx.compose.extended)
api(libs.bundles.koin)
implementation(libs.bundles.play.update)
implementation(libs.kotlin.stdlib)
implementation(libs.kotlinx.coroutines.android)
@ -114,12 +115,12 @@ dependencies {
implementation(projects.buildInfoLib)
implementation(projects.configurationApiLib)
implementation(projects.configurationImplAndroidLib)
implementation(projects.crashAndroidLib)
implementation(projects.preferenceApiLib)
implementation(projects.preferenceImplAndroidLib)
implementation(projects.sdkExtLib)
implementation(projects.spackleAndroidLib)
api(projects.configurationImplAndroidLib)
api(projects.sdkExtLib)
api(projects.uiDesignLib)
androidTestImplementation(projects.testLib)

View File

@ -1,26 +0,0 @@
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 EncryptedPreferenceKeysTest {
// This test is primary to prevent copy-paste errors in preference keys
@SmallTest
@Test
fun unique_keys() {
val fieldValueSet = mutableSetOf<String>()
EncryptedPreferenceKeys::class.memberProperties
.map { it.getter.call(EncryptedPreferenceKeys) }
.map { it as PreferenceDefault<*> }
.map { it.key }
.forEach {
assertFalse(fieldValueSet.contains(it.key), "Duplicate key $it")
fieldValueSet.add(it.key)
}
}
}

View File

@ -1,5 +1,6 @@
package co.electriccoin.zcash.ui.screen.support.model
import co.electriccoin.zcash.configuration.AndroidConfigurationFactory
import co.electriccoin.zcash.ui.test.getAppContext
import kotlinx.coroutines.test.runTest
import org.junit.Test
@ -10,7 +11,7 @@ class SupportInfoTest {
@Test
fun filter_time() =
runTest {
val supportInfo = SupportInfo.new(getAppContext())
val supportInfo = SupportInfo.new(getAppContext(), AndroidConfigurationFactory.new())
val individualExpected = supportInfo.timeInfo.toSupportString()
@ -24,7 +25,7 @@ class SupportInfoTest {
@Test
fun filter_app() =
runTest {
val supportInfo = SupportInfo.new(getAppContext())
val supportInfo = SupportInfo.new(getAppContext(), AndroidConfigurationFactory.new())
val individualExpected = supportInfo.appInfo.toSupportString()
@ -38,7 +39,7 @@ class SupportInfoTest {
@Test
fun filter_os() =
runTest {
val supportInfo = SupportInfo.new(getAppContext())
val supportInfo = SupportInfo.new(getAppContext(), AndroidConfigurationFactory.new())
val individualExpected = supportInfo.operatingSystemInfo.toSupportString()
@ -52,7 +53,7 @@ class SupportInfoTest {
@Test
fun filter_device() =
runTest {
val supportInfo = SupportInfo.new(getAppContext())
val supportInfo = SupportInfo.new(getAppContext(), AndroidConfigurationFactory.new())
val individualExpected = supportInfo.deviceInfo.toSupportString()
@ -66,7 +67,7 @@ class SupportInfoTest {
@Test
fun filter_crash() =
runTest {
val supportInfo = SupportInfo.new(getAppContext())
val supportInfo = SupportInfo.new(getAppContext(), AndroidConfigurationFactory.new())
val individualExpected = supportInfo.crashInfo.toCrashSupportString()
@ -77,7 +78,7 @@ class SupportInfoTest {
@Test
fun filter_environment() =
runTest {
val supportInfo = SupportInfo.new(getAppContext())
val supportInfo = SupportInfo.new(getAppContext(), AndroidConfigurationFactory.new())
val individualExpected = supportInfo.environmentInfo.toSupportString()
@ -91,7 +92,7 @@ class SupportInfoTest {
@Test
fun filter_permission() =
runTest {
val supportInfo = SupportInfo.new(getAppContext())
val supportInfo = SupportInfo.new(getAppContext(), AndroidConfigurationFactory.new())
val individualExpected = supportInfo.permissionInfo.toPermissionSupportString()

View File

@ -6,7 +6,7 @@ import androidx.activity.ComponentActivity
import androidx.compose.ui.test.junit4.createAndroidComposeRule
import androidx.test.filters.MediumTest
import cash.z.ecc.android.sdk.ext.onFirst
import co.electriccoin.zcash.ui.screen.update.AppUpdateCheckerImp
import co.electriccoin.zcash.ui.screen.update.AppUpdateCheckerImpl
import co.electriccoin.zcash.ui.screen.update.model.UpdateInfo
import co.electriccoin.zcash.ui.screen.update.model.UpdateState
import co.electriccoin.zcash.ui.test.getAppContext
@ -19,13 +19,13 @@ import kotlin.test.assertNotNull
import kotlin.test.assertNull
import kotlin.test.assertTrue
class AppUpdateCheckerImpTest {
class AppUpdateCheckerImplTest {
@get:Rule
val composeTestRule = createAndroidComposeRule<ComponentActivity>()
companion object {
val context: Context = getAppContext()
val updateChecker = AppUpdateCheckerImp.new()
val updateChecker = AppUpdateCheckerImpl()
}
private fun getAppUpdateInfoFlow(): Flow<UpdateInfo> {

View File

@ -23,8 +23,8 @@ class UpdateViewAndroidTest : UiTestPrerequisites() {
private fun newTestSetup(updateInfo: UpdateInfo) =
UpdateViewAndroidTestSetup(
composeTestRule,
updateInfo
updateInfo,
composeTestRule
).apply {
setDefaultContent()
}

View File

@ -3,21 +3,36 @@ package co.electriccoin.zcash.ui.screen.update.view
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.ui.test.junit4.AndroidComposeTestRule
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import co.electriccoin.zcash.ui.common.compose.LocalActivity
import co.electriccoin.zcash.ui.design.theme.ZcashTheme
import co.electriccoin.zcash.ui.screen.update.AppUpdateCheckerMock
import co.electriccoin.zcash.ui.screen.update.WrapUpdate
import co.electriccoin.zcash.ui.screen.update.model.UpdateInfo
import co.electriccoin.zcash.ui.screen.update.viewmodel.UpdateViewModel
class UpdateViewAndroidTestSetup(
private val composeTestRule: AndroidComposeTestRule<*, *>,
private val updateInfo: UpdateInfo
updateInfo: UpdateInfo,
private val composeTestRule: AndroidComposeTestRule<*, *>
) {
private val viewModel =
UpdateViewModel(
application = composeTestRule.activity.application,
updateInfo = updateInfo,
appUpdateChecker = AppUpdateCheckerMock.new()
)
@Composable
@Suppress("TestFunctionName")
fun DefaultContent() {
CompositionLocalProvider(LocalActivity provides composeTestRule.activity) {
val updateInfo = viewModel.updateInfo.collectAsStateWithLifecycle().value
// val appUpdateInfo = updateInfo.appUpdateInfo
WrapUpdate(
updateInfo
updateInfo = updateInfo,
checkForUpdate = viewModel::checkForAppUpdate,
remindLater = viewModel::remindLater,
goForUpdate = {}
)
}
}

View File

@ -0,0 +1,40 @@
package co.electriccoin.zcash.di
import androidx.biometric.BiometricManager
import cash.z.ecc.android.sdk.WalletCoordinator
import co.electriccoin.zcash.configuration.AndroidConfigurationFactory
import co.electriccoin.zcash.global.newInstance
import co.electriccoin.zcash.preference.EncryptedPreferenceProvider
import co.electriccoin.zcash.preference.StandardPreferenceProvider
import co.electriccoin.zcash.preference.model.entry.PreferenceKey
import co.electriccoin.zcash.ui.preference.PersistableWalletPreferenceDefault
import co.electriccoin.zcash.ui.screen.update.AppUpdateChecker
import co.electriccoin.zcash.ui.screen.update.AppUpdateCheckerImpl
import org.koin.core.module.dsl.factoryOf
import org.koin.core.module.dsl.singleOf
import org.koin.dsl.bind
import org.koin.dsl.module
val coreModule =
module {
single {
WalletCoordinator.newInstance(
context = get(),
encryptedPreferenceProvider = get(),
persistableWalletPreference = get(),
)
}
single {
PersistableWalletPreferenceDefault(PreferenceKey("persistable_wallet"))
}
singleOf(::StandardPreferenceProvider)
singleOf(::EncryptedPreferenceProvider)
single { BiometricManager.from(get()) }
factoryOf(::AppUpdateCheckerImpl) bind AppUpdateChecker::class
factory { AndroidConfigurationFactory.new() }
}

View File

@ -0,0 +1,47 @@
package co.electriccoin.zcash.di
import android.content.Context
import android.content.ContextWrapper
import androidx.activity.ComponentActivity
import androidx.compose.runtime.Composable
import androidx.compose.runtime.ProvidableCompositionLocal
import androidx.compose.ui.platform.LocalContext
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelStoreOwner
import androidx.lifecycle.viewmodel.CreationExtras
import org.koin.androidx.compose.defaultExtras
import org.koin.androidx.compose.koinViewModel
import org.koin.compose.currentKoinScope
import org.koin.core.parameter.ParametersDefinition
import org.koin.core.qualifier.Qualifier
import org.koin.core.scope.Scope
@Suppress("LongParameterList")
@Composable
inline fun <reified T : ViewModel> koinActivityViewModel(
qualifier: Qualifier? = null,
viewModelStoreOwner: ViewModelStoreOwner = LocalContext.componentActivity(),
key: String? = null,
extras: CreationExtras = defaultExtras(LocalContext.componentActivity()),
scope: Scope = currentKoinScope(),
noinline parameters: ParametersDefinition? = null,
) = koinViewModel<T>(
qualifier = qualifier,
viewModelStoreOwner = viewModelStoreOwner,
key = key,
extras = extras,
scope = scope,
parameters = parameters,
)
@Composable
fun ProvidableCompositionLocal<Context>.componentActivity(): ComponentActivity {
val context = this.current
return when {
context is ComponentActivity -> context
context is ContextWrapper && context.baseContext is ComponentActivity ->
context.baseContext as ComponentActivity
else -> throw ClassCastException("Context is not a ComponentActivity")
}
}

View File

@ -0,0 +1,12 @@
package co.electriccoin.zcash.di
import co.electriccoin.zcash.ui.common.repository.WalletRepository
import co.electriccoin.zcash.ui.common.repository.WalletRepositoryImpl
import org.koin.core.module.dsl.singleOf
import org.koin.dsl.bind
import org.koin.dsl.module
val repositoryModule =
module {
singleOf(::WalletRepositoryImpl) bind WalletRepository::class
}

View File

@ -0,0 +1,12 @@
package co.electriccoin.zcash.di
import co.electriccoin.zcash.ui.common.usecase.GetSynchronizerUseCase
import co.electriccoin.zcash.ui.common.usecase.ObserveSynchronizerUseCase
import org.koin.core.module.dsl.singleOf
import org.koin.dsl.module
val useCaseModule =
module {
singleOf(::ObserveSynchronizerUseCase)
singleOf(::GetSynchronizerUseCase)
}

View File

@ -0,0 +1,38 @@
package co.electriccoin.zcash.di
import co.electriccoin.zcash.ui.common.viewmodel.AuthenticationViewModel
import co.electriccoin.zcash.ui.common.viewmodel.CheckUpdateViewModel
import co.electriccoin.zcash.ui.common.viewmodel.HomeViewModel
import co.electriccoin.zcash.ui.common.viewmodel.WalletViewModel
import co.electriccoin.zcash.ui.screen.account.viewmodel.TransactionHistoryViewModel
import co.electriccoin.zcash.ui.screen.onboarding.viewmodel.OnboardingViewModel
import co.electriccoin.zcash.ui.screen.restore.viewmodel.RestoreViewModel
import co.electriccoin.zcash.ui.screen.restoresuccess.viewmodel.RestoreSuccessViewModel
import co.electriccoin.zcash.ui.screen.sendconfirmation.viewmodel.CreateTransactionsViewModel
import co.electriccoin.zcash.ui.screen.settings.viewmodel.ScreenBrightnessViewModel
import co.electriccoin.zcash.ui.screen.settings.viewmodel.SettingsViewModel
import co.electriccoin.zcash.ui.screen.support.viewmodel.SupportViewModel
import co.electriccoin.zcash.ui.screen.update.viewmodel.UpdateViewModel
import co.electriccoin.zcash.ui.screen.warning.viewmodel.StorageCheckViewModel
import co.electriccoin.zcash.ui.screen.whatsnew.viewmodel.WhatsNewViewModel
import org.koin.androidx.viewmodel.dsl.viewModelOf
import org.koin.dsl.module
val viewModelModule =
module {
viewModelOf(::WalletViewModel)
viewModelOf(::AuthenticationViewModel)
viewModelOf(::CheckUpdateViewModel)
viewModelOf(::HomeViewModel)
viewModelOf(::TransactionHistoryViewModel)
viewModelOf(::OnboardingViewModel)
viewModelOf(::StorageCheckViewModel)
viewModelOf(::RestoreViewModel)
viewModelOf(::ScreenBrightnessViewModel)
viewModelOf(::SettingsViewModel)
viewModelOf(::SupportViewModel)
viewModelOf(::CreateTransactionsViewModel)
viewModelOf(::RestoreSuccessViewModel)
viewModelOf(::WhatsNewViewModel)
viewModelOf(::UpdateViewModel)
}

View File

@ -2,30 +2,21 @@ package co.electriccoin.zcash.global
import android.content.Context
import cash.z.ecc.android.sdk.WalletCoordinator
import co.electriccoin.zcash.spackle.LazyWithArgument
import co.electriccoin.zcash.ui.preference.EncryptedPreferenceKeys
import co.electriccoin.zcash.ui.preference.EncryptedPreferenceSingleton
import co.electriccoin.zcash.preference.EncryptedPreferenceProvider
import co.electriccoin.zcash.ui.preference.PersistableWalletPreferenceDefault
import kotlinx.coroutines.flow.emitAll
import kotlinx.coroutines.flow.flow
private val lazy =
LazyWithArgument<Context, WalletCoordinator> {
/**
* A flow of the user's stored wallet. Null indicates that no wallet has been stored.
*/
val persistableWallet =
fun WalletCoordinator.Companion.newInstance(
context: Context,
encryptedPreferenceProvider: EncryptedPreferenceProvider,
persistableWalletPreference: PersistableWalletPreferenceDefault
): WalletCoordinator {
return WalletCoordinator(
context = context,
persistableWallet =
flow {
// EncryptedPreferenceSingleton.getInstance() is a suspending function, which is why we need
// the flow builder to provide a coroutine context.
val encryptedPreferenceProvider = EncryptedPreferenceSingleton.getInstance(it)
emitAll(EncryptedPreferenceKeys.PERSISTABLE_WALLET.observe(encryptedPreferenceProvider))
emitAll(persistableWalletPreference.observe(encryptedPreferenceProvider()))
}
WalletCoordinator(
context = it,
persistableWallet = persistableWallet
)
}
fun WalletCoordinator.Companion.getInstance(context: Context) = lazy.getInstance(context)

View File

@ -5,7 +5,6 @@ import android.content.pm.ActivityInfo
import android.os.Bundle
import android.os.SystemClock
import androidx.activity.enableEdgeToEdge
import androidx.activity.viewModels
import androidx.annotation.VisibleForTesting
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth
@ -58,20 +57,19 @@ import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
import org.koin.androidx.viewmodel.ext.android.viewModel
import kotlin.time.Duration
import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.Duration.Companion.seconds
class MainActivity : FragmentActivity() {
private val homeViewModel by viewModels<HomeViewModel>()
private val homeViewModel by viewModel<HomeViewModel>()
val walletViewModel by viewModels<WalletViewModel>()
val walletViewModel by viewModel<WalletViewModel>()
val storageCheckViewModel by viewModels<StorageCheckViewModel>()
val storageCheckViewModel by viewModel<StorageCheckViewModel>()
internal val authenticationViewModel by viewModels<AuthenticationViewModel> {
AuthenticationViewModel.AuthenticationViewModelFactory(application)
}
internal val authenticationViewModel by viewModel<AuthenticationViewModel>()
lateinit var navControllerForTesting: NavHostController
@ -276,7 +274,6 @@ class MainActivity : FragmentActivity() {
private fun monitorForBackgroundSync() {
val isEnableBackgroundSyncFlow =
run {
val homeViewModel by viewModels<HomeViewModel>()
val isSecretReadyFlow = walletViewModel.secretState.map { it is SecretState.Ready }
val isBackgroundSyncEnabledFlow = homeViewModel.isBackgroundSyncEnabled.filterNotNull()

View File

@ -1,6 +0,0 @@
package co.electriccoin.zcash.ui.common
import kotlin.time.Duration.Companion.seconds
// Recommended timeout for Android configuration changes to keep Kotlin Flow from restarting
val ANDROID_STATE_FLOW_TIMEOUT = 5.seconds

View File

@ -33,7 +33,7 @@ fun MainActivity.BindCompLocalProvider(content: @Composable () -> Unit) {
LocalScreenBrightness provides screenBrightness,
LocalScreenTimeout provides screenTimeout,
LocalNavController provides navController,
LocalActivity provides this,
LocalActivity provides this@BindCompLocalProvider,
content = content
)
}

View File

@ -0,0 +1,29 @@
package co.electriccoin.zcash.ui.common.repository
import cash.z.ecc.android.sdk.Synchronizer
import cash.z.ecc.android.sdk.WalletCoordinator
import cash.z.ecc.sdk.ANDROID_STATE_FLOW_TIMEOUT
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.WhileSubscribed
import kotlinx.coroutines.flow.stateIn
interface WalletRepository {
val synchronizer: StateFlow<Synchronizer?>
}
class WalletRepositoryImpl(
walletCoordinator: WalletCoordinator,
) : WalletRepository {
private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
override val synchronizer: StateFlow<Synchronizer?> =
walletCoordinator.synchronizer.stateIn(
scope = scope,
started = SharingStarted.WhileSubscribed(ANDROID_STATE_FLOW_TIMEOUT),
initialValue = null
)
}

View File

@ -0,0 +1,11 @@
package co.electriccoin.zcash.ui.common.usecase
import co.electriccoin.zcash.ui.common.repository.WalletRepository
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.first
class GetSynchronizerUseCase(
private val walletRepository: WalletRepository
) {
suspend operator fun invoke() = walletRepository.synchronizer.filterNotNull().first()
}

View File

@ -0,0 +1,9 @@
package co.electriccoin.zcash.ui.common.usecase
import co.electriccoin.zcash.ui.common.repository.WalletRepository
class ObserveSynchronizerUseCase(
private val walletRepository: WalletRepository
) {
operator fun invoke() = walletRepository.synchronizer
}

View File

@ -7,17 +7,15 @@ import androidx.biometric.BiometricManager.Authenticators
import androidx.biometric.BiometricPrompt
import androidx.core.content.ContextCompat
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewModelScope
import cash.z.ecc.sdk.ANDROID_STATE_FLOW_TIMEOUT
import co.electriccoin.zcash.preference.StandardPreferenceProvider
import co.electriccoin.zcash.preference.model.entry.BooleanPreferenceDefault
import co.electriccoin.zcash.spackle.AndroidApiVersion
import co.electriccoin.zcash.spackle.Twig
import co.electriccoin.zcash.ui.MainActivity
import co.electriccoin.zcash.ui.R
import co.electriccoin.zcash.ui.common.ANDROID_STATE_FLOW_TIMEOUT
import co.electriccoin.zcash.ui.preference.StandardPreferenceKeys
import co.electriccoin.zcash.ui.preference.StandardPreferenceSingleton
import co.electriccoin.zcash.ui.screen.authentication.AuthenticationUseCase
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
@ -37,7 +35,9 @@ import kotlin.time.Duration.Companion.milliseconds
private const val DEFAULT_INITIAL_DELAY = 0
class AuthenticationViewModel(
private val application: Application,
application: Application,
private val standardPreferenceProvider: StandardPreferenceProvider,
private val biometricManager: BiometricManager,
) : AndroidViewModel(application) {
private val executor: Executor by lazy { ContextCompat.getMainExecutor(application) }
private lateinit var biometricPrompt: BiometricPrompt
@ -54,6 +54,7 @@ class AuthenticationViewModel(
// Android SDK version == 28 || 29
(AndroidApiVersion.isExactlyP || AndroidApiVersion.isExactlyQ) ->
Authenticators.BIOMETRIC_WEAK or Authenticators.DEVICE_CREDENTIAL
else -> error("Unsupported Android SDK version")
}
@ -128,6 +129,7 @@ class AuthenticationViewModel(
BiometricSupportResult.Success -> {
// No action needed, let user proceed to the authentication steps
}
else -> {
// Otherwise biometric authentication might not be available, but users still can use the
// device credential authentication path
@ -253,24 +255,28 @@ class AuthenticationViewModel(
promptInfo =
BiometricPrompt.PromptInfo.Builder()
.setTitle(
application.applicationContext.run {
getApplication<Application>().applicationContext.run {
getString(R.string.authentication_system_ui_title, getString(R.string.app_name))
}
)
.setSubtitle(
application.applicationContext.run {
getApplication<Application>().applicationContext.run {
getString(
R.string.authentication_system_ui_subtitle,
getString(
when (useCase) {
AuthenticationUseCase.AppAccess ->
R.string.app_name
AuthenticationUseCase.DeleteWallet ->
R.string.authentication_use_case_delete_wallet
AuthenticationUseCase.ExportPrivateData ->
R.string.authentication_use_case_export_data
AuthenticationUseCase.SeedRecovery ->
R.string.authentication_use_case_seed_recovery
AuthenticationUseCase.SendFunds ->
R.string.authentication_use_case_send_funds
}
@ -292,13 +298,12 @@ class AuthenticationViewModel(
}
private fun getBiometricAuthenticationSupport(allowedAuthenticators: Int): BiometricSupportResult {
val biometricManager = BiometricManager.from(application)
return when (biometricManager.canAuthenticate(allowedAuthenticators)) {
BiometricManager.BIOMETRIC_SUCCESS -> {
Twig.debug { "Auth canAuthenticate BIOMETRIC_SUCCESS: App can authenticate using biometrics." }
BiometricSupportResult.Success
}
BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE -> {
Twig.info {
"Auth canAuthenticate BIOMETRIC_ERROR_NO_HARDWARE: No biometric features available on " +
@ -306,6 +311,7 @@ class AuthenticationViewModel(
}
BiometricSupportResult.ErrorNoHardware
}
BiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE -> {
Twig.error {
"Auth canAuthenticate BIOMETRIC_ERROR_HW_UNAVAILABLE: Biometric features are currently " +
@ -313,6 +319,7 @@ class AuthenticationViewModel(
}
BiometricSupportResult.ErrorHwUnavailable
}
BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED -> {
Twig.warn {
"Auth canAuthenticate BIOMETRIC_ERROR_NONE_ENROLLED: Prompts the user to create " +
@ -320,6 +327,7 @@ class AuthenticationViewModel(
}
BiometricSupportResult.ErrorNoneEnrolled
}
BiometricManager.BIOMETRIC_ERROR_SECURITY_UPDATE_REQUIRED -> {
Twig.error {
"Auth canAuthenticate BIOMETRIC_ERROR_SECURITY_UPDATE_REQUIRED: The user can't authenticate " +
@ -328,6 +336,7 @@ class AuthenticationViewModel(
}
BiometricSupportResult.ErrorSecurityUpdateRequired
}
BiometricManager.BIOMETRIC_ERROR_UNSUPPORTED -> {
Twig.error {
"Auth canAuthenticate BIOMETRIC_ERROR_UNSUPPORTED: The user can't authenticate because " +
@ -335,6 +344,7 @@ class AuthenticationViewModel(
}
BiometricSupportResult.ErrorUnsupported
}
BiometricManager.BIOMETRIC_STATUS_UNKNOWN -> {
Twig.error {
"Auth canAuthenticate BIOMETRIC_STATUS_UNKNOWN: Unable to determine whether the user can" +
@ -345,6 +355,7 @@ class AuthenticationViewModel(
}
BiometricSupportResult.StatusUnknown
}
else -> {
Twig.error { "Unexpected biometric framework status" }
BiometricSupportResult.StatusUnexpected
@ -352,20 +363,9 @@ class AuthenticationViewModel(
}
}
@Suppress("UNCHECKED_CAST")
class AuthenticationViewModelFactory(
private val application: Application
) : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
require(modelClass.isAssignableFrom(AuthenticationViewModel::class.java)) { "ViewModel Not Found." }
return AuthenticationViewModel(application) as T
}
}
private fun booleanStateFlow(default: BooleanPreferenceDefault): StateFlow<Boolean?> =
flow<Boolean?> {
val preferenceProvider = StandardPreferenceSingleton.getInstance(getApplication())
emitAll(default.observe(preferenceProvider))
emitAll(default.observe(standardPreferenceProvider()))
}.stateIn(
viewModelScope,
SharingStarted.WhileSubscribed(ANDROID_STATE_FLOW_TIMEOUT),

View File

@ -2,8 +2,6 @@ package co.electriccoin.zcash.ui.common.viewmodel
import android.app.Application
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewModelScope
import cash.z.ecc.android.sdk.ext.onFirst
import co.electriccoin.zcash.ui.screen.update.AppUpdateChecker
@ -26,18 +24,4 @@ class CheckUpdateViewModel(
}
}
}
@Suppress("UNCHECKED_CAST")
class CheckUpdateViewModelFactory(
private val application: Application,
private val appUpdateChecker: AppUpdateChecker
) : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
return if (modelClass.isAssignableFrom(CheckUpdateViewModel::class.java)) {
CheckUpdateViewModel(application, appUpdateChecker) as T
} else {
throw IllegalArgumentException("ViewModel Not Found.")
}
}
}
}

View File

@ -1,14 +1,13 @@
package co.electriccoin.zcash.ui.common.viewmodel
import android.app.Application
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import co.electriccoin.zcash.configuration.AndroidConfigurationFactory
import cash.z.ecc.sdk.ANDROID_STATE_FLOW_TIMEOUT
import co.electriccoin.zcash.configuration.api.ConfigurationProvider
import co.electriccoin.zcash.configuration.model.map.Configuration
import co.electriccoin.zcash.preference.StandardPreferenceProvider
import co.electriccoin.zcash.preference.model.entry.BooleanPreferenceDefault
import co.electriccoin.zcash.ui.common.ANDROID_STATE_FLOW_TIMEOUT
import co.electriccoin.zcash.ui.preference.StandardPreferenceKeys
import co.electriccoin.zcash.ui.preference.StandardPreferenceSingleton
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.WhileSubscribed
@ -19,7 +18,10 @@ import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
class HomeViewModel(application: Application) : AndroidViewModel(application) {
class HomeViewModel(
androidConfigurationProvider: ConfigurationProvider,
private val standardPreferenceProvider: StandardPreferenceProvider,
) : ViewModel() {
/**
* A flow of whether background sync is enabled
*/
@ -54,7 +56,7 @@ class HomeViewModel(application: Application) : AndroidViewModel(application) {
}
val configurationFlow: StateFlow<Configuration?> =
AndroidConfigurationFactory.getInstance(application).getConfigurationFlow()
androidConfigurationProvider.getConfigurationFlow()
.stateIn(
viewModelScope,
SharingStarted.WhileSubscribed(ANDROID_STATE_FLOW_TIMEOUT.inWholeMilliseconds),
@ -67,8 +69,7 @@ class HomeViewModel(application: Application) : AndroidViewModel(application) {
private fun booleanStateFlow(default: BooleanPreferenceDefault): StateFlow<Boolean?> =
flow<Boolean?> {
val preferenceProvider = StandardPreferenceSingleton.getInstance(getApplication())
emitAll(default.observe(preferenceProvider))
emitAll(default.observe(standardPreferenceProvider()))
}.stateIn(
viewModelScope,
SharingStarted.WhileSubscribed(ANDROID_STATE_FLOW_TIMEOUT),
@ -80,8 +81,7 @@ class HomeViewModel(application: Application) : AndroidViewModel(application) {
newState: Boolean
) {
viewModelScope.launch {
val prefs = StandardPreferenceSingleton.getInstance(getApplication())
default.putValue(prefs, newState)
default.putValue(standardPreferenceProvider(), newState)
}
}
}

View File

@ -24,13 +24,14 @@ import cash.z.ecc.android.sdk.model.WalletBalance
import cash.z.ecc.android.sdk.model.Zatoshi
import cash.z.ecc.android.sdk.model.ZcashNetwork
import cash.z.ecc.android.sdk.tool.DerivationTool
import cash.z.ecc.sdk.ANDROID_STATE_FLOW_TIMEOUT
import cash.z.ecc.sdk.type.fromResources
import co.electriccoin.zcash.global.getInstance
import co.electriccoin.zcash.preference.EncryptedPreferenceProvider
import co.electriccoin.zcash.preference.StandardPreferenceProvider
import co.electriccoin.zcash.preference.model.entry.NullableBooleanPreferenceDefault
import co.electriccoin.zcash.spackle.Twig
import co.electriccoin.zcash.ui.MainActivity
import co.electriccoin.zcash.ui.NavigationTargets.EXCHANGE_RATE_OPT_IN
import co.electriccoin.zcash.ui.common.ANDROID_STATE_FLOW_TIMEOUT
import co.electriccoin.zcash.ui.common.compose.BalanceState
import co.electriccoin.zcash.ui.common.extension.throttle
import co.electriccoin.zcash.ui.common.model.OnboardingState
@ -41,13 +42,12 @@ import co.electriccoin.zcash.ui.common.model.hasChangePending
import co.electriccoin.zcash.ui.common.model.hasValuePending
import co.electriccoin.zcash.ui.common.model.spendableBalance
import co.electriccoin.zcash.ui.common.model.totalBalance
import co.electriccoin.zcash.ui.common.usecase.ObserveSynchronizerUseCase
import co.electriccoin.zcash.ui.common.wallet.ExchangeRateState
import co.electriccoin.zcash.ui.common.wallet.RefreshLock
import co.electriccoin.zcash.ui.common.wallet.StaleLock
import co.electriccoin.zcash.ui.preference.EncryptedPreferenceKeys
import co.electriccoin.zcash.ui.preference.EncryptedPreferenceSingleton
import co.electriccoin.zcash.ui.preference.PersistableWalletPreferenceDefault
import co.electriccoin.zcash.ui.preference.StandardPreferenceKeys
import co.electriccoin.zcash.ui.preference.StandardPreferenceSingleton
import co.electriccoin.zcash.ui.screen.account.ext.TransactionOverviewExt
import co.electriccoin.zcash.ui.screen.account.ext.getSortHeight
import co.electriccoin.zcash.ui.screen.account.state.TransactionHistorySyncState
@ -88,12 +88,17 @@ import kotlin.time.Duration.Companion.seconds
// for loading the preferences.
// TODO [#292]: Should be moved to SDK-EXT-UI module.
// TODO [#292]: https://github.com/Electric-Coin-Company/zashi-android/issues/292
@Suppress("TooManyFunctions")
@Suppress(
"TooManyFunctions"
)
class WalletViewModel(
application: Application
application: Application,
observeSynchronizer: ObserveSynchronizerUseCase,
private val persistableWalletPreference: PersistableWalletPreferenceDefault,
private val walletCoordinator: WalletCoordinator,
private val encryptedPreferenceProvider: EncryptedPreferenceProvider,
private val standardPreferenceProvider: StandardPreferenceProvider,
) : AndroidViewModel(application) {
private val walletCoordinator = WalletCoordinator.getInstance(application)
/*
* Using the Mutex may be overkill, but it ensures that if multiple calls are accidentally made
* that they have a consistent ordering.
@ -107,21 +112,16 @@ class WalletViewModel(
/**
* Synchronizer that is retained long enough to survive configuration changes.
*/
val synchronizer =
walletCoordinator.synchronizer.stateIn(
viewModelScope,
SharingStarted.WhileSubscribed(ANDROID_STATE_FLOW_TIMEOUT),
null
)
val synchronizer = observeSynchronizer()
/**
* A flow of the wallet block synchronization state.
*/
val walletRestoringState: StateFlow<WalletRestoringState> =
flow {
val preferenceProvider = StandardPreferenceSingleton.getInstance(application)
emitAll(
StandardPreferenceKeys.WALLET_RESTORING_STATE.observe(preferenceProvider).map { persistedNumber ->
StandardPreferenceKeys.WALLET_RESTORING_STATE
.observe(standardPreferenceProvider()).map { persistedNumber ->
WalletRestoringState.fromNumber(persistedNumber)
}
)
@ -162,9 +162,9 @@ class WalletViewModel(
*/
private val onboardingState =
flow {
val preferenceProvider = StandardPreferenceSingleton.getInstance(application)
emitAll(
StandardPreferenceKeys.ONBOARDING_STATE.observe(preferenceProvider).map { persistedNumber ->
StandardPreferenceKeys.ONBOARDING_STATE
.observe(standardPreferenceProvider()).map { persistedNumber ->
OnboardingState.fromNumber(persistedNumber)
}
)
@ -510,12 +510,9 @@ class WalletViewModel(
* Persists a wallet asynchronously. Clients observe [secretState] to see the side effects.
*/
private fun persistWallet(persistableWallet: PersistableWallet) {
val application = getApplication<Application>()
viewModelScope.launch {
val preferenceProvider = EncryptedPreferenceSingleton.getInstance(application)
persistWalletMutex.withLock {
EncryptedPreferenceKeys.PERSISTABLE_WALLET.putValue(preferenceProvider, persistableWallet)
persistableWalletPreference.putValue(encryptedPreferenceProvider(), persistableWallet)
}
}
}
@ -526,17 +523,17 @@ class WalletViewModel(
* for a user creating a new wallet.
*/
fun persistOnboardingState(onboardingState: OnboardingState) {
val application = getApplication<Application>()
viewModelScope.launch {
val preferenceProvider = StandardPreferenceSingleton.getInstance(application)
// Use the Mutex here to avoid timing issues. During wallet restore, persistOnboardingState()
// is called prior to persistExistingWallet(). Although persistOnboardingState() should
// complete quickly, it isn't guaranteed to complete before persistExistingWallet()
// unless a mutex is used here.
persistWalletMutex.withLock {
StandardPreferenceKeys.ONBOARDING_STATE.putValue(preferenceProvider, onboardingState.toNumber())
StandardPreferenceKeys.ONBOARDING_STATE.putValue(
standardPreferenceProvider(),
onboardingState
.toNumber()
)
}
}
}
@ -548,11 +545,11 @@ class WalletViewModel(
* state from the SDK, and thus, we need to note the wallet restoring state here on the client side.
*/
fun persistWalletRestoringState(walletRestoringState: WalletRestoringState) {
val application = getApplication<Application>()
viewModelScope.launch {
val preferenceProvider = StandardPreferenceSingleton.getInstance(application)
StandardPreferenceKeys.WALLET_RESTORING_STATE.putValue(preferenceProvider, walletRestoringState.toNumber())
StandardPreferenceKeys.WALLET_RESTORING_STATE.putValue(
standardPreferenceProvider(),
walletRestoringState.toNumber()
)
}
}
@ -589,16 +586,12 @@ class WalletViewModel(
private fun clearAppStateFlow(): Flow<Boolean> =
callbackFlow {
val application = getApplication<Application>()
viewModelScope.launch {
val standardPrefsCleared =
StandardPreferenceSingleton
.getInstance(application)
standardPreferenceProvider()
.clearPreferences()
val encryptedPrefsCleared =
EncryptedPreferenceSingleton
.getInstance(application)
encryptedPreferenceProvider()
.clearPreferences()
Twig.info { "Both preferences cleared: ${standardPrefsCleared && encryptedPrefsCleared}" }
@ -650,8 +643,7 @@ class WalletViewModel(
private fun nullableBooleanStateFlow(default: NullableBooleanPreferenceDefault): StateFlow<Boolean?> =
flow {
val preferenceProvider = StandardPreferenceSingleton.getInstance(getApplication())
emitAll(default.observe(preferenceProvider))
emitAll(default.observe(standardPreferenceProvider()))
}.stateIn(
viewModelScope,
SharingStarted.WhileSubscribed(ANDROID_STATE_FLOW_TIMEOUT),
@ -663,8 +655,7 @@ class WalletViewModel(
newState: Boolean
) {
viewModelScope.launch {
val prefs = StandardPreferenceSingleton.getInstance(getApplication())
default.putValue(prefs, newState)
default.putValue(standardPreferenceProvider(), newState)
}
}
}

View File

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

View File

@ -1,17 +0,0 @@
package co.electriccoin.zcash.ui.preference
import android.content.Context
import co.electriccoin.zcash.preference.AndroidPreferenceProvider
import co.electriccoin.zcash.preference.api.PreferenceProvider
import co.electriccoin.zcash.spackle.SuspendingLazy
object EncryptedPreferenceSingleton {
private const val PREF_FILENAME = "co.electriccoin.zcash.encrypted"
private val lazy =
SuspendingLazy<Context, PreferenceProvider> {
AndroidPreferenceProvider.newEncrypted(it, PREF_FILENAME)
}
suspend fun getInstance(context: Context) = lazy.getInstance(context)
}

View File

@ -1,17 +0,0 @@
package co.electriccoin.zcash.ui.preference
import android.content.Context
import co.electriccoin.zcash.preference.AndroidPreferenceProvider
import co.electriccoin.zcash.preference.api.PreferenceProvider
import co.electriccoin.zcash.spackle.SuspendingLazy
object StandardPreferenceSingleton {
private const val PREF_FILENAME = "co.electriccoin.zcash"
private val lazy =
SuspendingLazy<Context, PreferenceProvider> {
AndroidPreferenceProvider.newStandard(it, PREF_FILENAME)
}
suspend fun getInstance(context: Context) = lazy.getInstance(context)
}

View File

@ -4,14 +4,14 @@ package co.electriccoin.zcash.ui.screen.about
import android.content.Context
import androidx.activity.compose.BackHandler
import androidx.activity.viewModels
import androidx.compose.material3.SnackbarHostState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import co.electriccoin.zcash.configuration.AndroidConfigurationFactory
import co.electriccoin.zcash.configuration.api.ConfigurationProvider
import co.electriccoin.zcash.di.koinActivityViewModel
import co.electriccoin.zcash.ui.R
import co.electriccoin.zcash.ui.common.compose.LocalActivity
import co.electriccoin.zcash.ui.common.model.VersionInfo
@ -21,6 +21,7 @@ import co.electriccoin.zcash.ui.screen.about.view.About
import co.electriccoin.zcash.ui.screen.support.model.ConfigInfo
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import org.koin.compose.koinInject
@Composable
internal fun WrapAbout(
@ -29,7 +30,7 @@ internal fun WrapAbout(
) {
val activity = LocalActivity.current
val walletViewModel by activity.viewModels<WalletViewModel>()
val walletViewModel = koinActivityViewModel<WalletViewModel>()
val walletState = walletViewModel.walletStateInformation.collectAsStateWithLifecycle().value
@ -37,12 +38,13 @@ internal fun WrapAbout(
goBack()
}
val configInfo = ConfigInfo.new(AndroidConfigurationFactory.getInstance(activity.applicationContext))
val androidConfigurationProvider = koinInject<ConfigurationProvider>()
val configInfo = ConfigInfo.new(androidConfigurationProvider)
val versionInfo = VersionInfo.new(activity.applicationContext)
// Allows an implicit way to force configuration refresh by simply visiting the About screen
LaunchedEffect(key1 = true) {
AndroidConfigurationFactory.getInstance(activity.applicationContext).hintToRefresh()
androidConfigurationProvider.hintToRefresh()
}
val snackbarHostState = remember { SnackbarHostState() }

View File

@ -3,7 +3,6 @@
package co.electriccoin.zcash.ui.screen.account
import android.content.Context
import androidx.activity.viewModels
import androidx.compose.material3.SnackbarHostState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
@ -13,6 +12,7 @@ import androidx.compose.ui.platform.LocalContext
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import cash.z.ecc.android.sdk.Synchronizer
import cash.z.ecc.android.sdk.internal.Twig
import co.electriccoin.zcash.di.koinActivityViewModel
import co.electriccoin.zcash.spackle.ClipboardManagerUtil
import co.electriccoin.zcash.ui.R
import co.electriccoin.zcash.ui.common.compose.BalanceState
@ -40,11 +40,11 @@ internal fun WrapAccount(
) {
val activity = LocalActivity.current
val walletViewModel by activity.viewModels<WalletViewModel>()
val walletViewModel = koinActivityViewModel<WalletViewModel>()
val transactionHistoryViewModel by activity.viewModels<TransactionHistoryViewModel>()
val transactionHistoryViewModel = koinActivityViewModel<TransactionHistoryViewModel>()
val homeViewModel by activity.viewModels<HomeViewModel>()
val homeViewModel = koinActivityViewModel<HomeViewModel>()
val synchronizer = walletViewModel.synchronizer.collectAsStateWithLifecycle().value

View File

@ -1,13 +1,12 @@
package co.electriccoin.zcash.ui.screen.account.viewmodel
import android.app.Application
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import cash.z.ecc.android.sdk.Synchronizer
import cash.z.ecc.android.sdk.internal.Twig
import cash.z.ecc.android.sdk.model.FirstClassByteArray
import cash.z.ecc.android.sdk.model.TransactionOverview
import co.electriccoin.zcash.ui.common.ANDROID_STATE_FLOW_TIMEOUT
import cash.z.ecc.sdk.ANDROID_STATE_FLOW_TIMEOUT
import co.electriccoin.zcash.ui.screen.account.ext.TransactionOverviewExt
import co.electriccoin.zcash.ui.screen.account.model.TransactionUi
import co.electriccoin.zcash.ui.screen.account.model.TransactionUiState
@ -24,7 +23,7 @@ import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.toList
class TransactionHistoryViewModel(application: Application) : AndroidViewModel(application) {
class TransactionHistoryViewModel : ViewModel() {
private val state: MutableStateFlow<State> = MutableStateFlow(State.LOADING)
private val transactions: MutableStateFlow<ImmutableList<TransactionUi>> = MutableStateFlow(persistentListOf())

View File

@ -3,9 +3,9 @@
package co.electriccoin.zcash.ui.screen.advancedsettings
import androidx.activity.compose.BackHandler
import androidx.activity.viewModels
import androidx.compose.runtime.Composable
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import co.electriccoin.zcash.di.koinActivityViewModel
import co.electriccoin.zcash.ui.MainActivity
import co.electriccoin.zcash.ui.common.model.TopAppBarSubTitleState
import co.electriccoin.zcash.ui.common.viewmodel.WalletViewModel
@ -21,7 +21,7 @@ internal fun MainActivity.WrapAdvancedSettings(
goSeedRecovery: () -> Unit,
onCurrencyConversion: () -> Unit
) {
val walletViewModel by viewModels<WalletViewModel>()
val walletViewModel = koinActivityViewModel<WalletViewModel>()
val walletState = walletViewModel.walletStateInformation.collectAsStateWithLifecycle().value

View File

@ -3,11 +3,11 @@
package co.electriccoin.zcash.ui.screen.authentication
import android.widget.Toast
import androidx.activity.viewModels
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.res.stringResource
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import co.electriccoin.zcash.di.koinActivityViewModel
import co.electriccoin.zcash.spackle.Twig
import co.electriccoin.zcash.ui.MainActivity
import co.electriccoin.zcash.ui.R
@ -115,7 +115,7 @@ private fun WrapDeleteWalletAuth(
onCancel: () -> Unit,
onFailed: () -> Unit,
) {
val authenticationViewModel by activity.viewModels<AuthenticationViewModel>()
val authenticationViewModel = koinActivityViewModel<AuthenticationViewModel>()
val authenticationResult =
authenticationViewModel.authenticationResult
@ -188,7 +188,7 @@ private fun WrapAppExportPrivateDataAuth(
onCancel: () -> Unit,
onFailed: () -> Unit,
) {
val authenticationViewModel by activity.viewModels<AuthenticationViewModel>()
val authenticationViewModel = koinActivityViewModel<AuthenticationViewModel>()
val authenticationResult =
authenticationViewModel.authenticationResult
@ -261,7 +261,7 @@ private fun WrapSeedRecoveryAuth(
onCancel: () -> Unit,
onFailed: () -> Unit,
) {
val authenticationViewModel by activity.viewModels<AuthenticationViewModel>()
val authenticationViewModel = koinActivityViewModel<AuthenticationViewModel>()
val authenticationResult =
authenticationViewModel.authenticationResult
@ -335,7 +335,7 @@ private fun WrapSendFundsAuth(
onCancel: () -> Unit,
onFailed: () -> Unit,
) {
val authenticationViewModel by activity.viewModels<AuthenticationViewModel>()
val authenticationViewModel = koinActivityViewModel<AuthenticationViewModel>()
val authenticationResult =
authenticationViewModel.authenticationResult
@ -409,7 +409,7 @@ private fun WrapAppAccessAuth(
onCancel: () -> Unit,
onFailed: () -> Unit,
) {
val authenticationViewModel by activity.viewModels<AuthenticationViewModel>()
val authenticationViewModel = koinActivityViewModel<AuthenticationViewModel>()
val welcomeAnimVisibility = authenticationViewModel.showWelcomeAnimation.collectAsStateWithLifecycle().value

View File

@ -4,7 +4,6 @@ package co.electriccoin.zcash.ui.screen.balances
import android.content.Context
import android.widget.Toast
import androidx.activity.viewModels
import androidx.compose.material3.SnackbarHostState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
@ -18,6 +17,7 @@ import cash.z.ecc.android.sdk.SdkSynchronizer
import cash.z.ecc.android.sdk.Synchronizer
import cash.z.ecc.android.sdk.model.UnifiedSpendingKey
import cash.z.ecc.android.sdk.model.Zatoshi
import co.electriccoin.zcash.di.koinActivityViewModel
import co.electriccoin.zcash.spackle.Twig
import co.electriccoin.zcash.ui.R
import co.electriccoin.zcash.ui.common.compose.BalanceState
@ -36,7 +36,6 @@ import co.electriccoin.zcash.ui.screen.balances.model.StatusAction
import co.electriccoin.zcash.ui.screen.balances.view.Balances
import co.electriccoin.zcash.ui.screen.sendconfirmation.model.SubmitResult
import co.electriccoin.zcash.ui.screen.sendconfirmation.viewmodel.CreateTransactionsViewModel
import co.electriccoin.zcash.ui.screen.update.AppUpdateCheckerImp
import co.electriccoin.zcash.ui.screen.update.model.UpdateState
import co.electriccoin.zcash.ui.util.PlayStoreUtil
import kotlinx.coroutines.CoroutineScope
@ -51,11 +50,11 @@ internal fun WrapBalances(
) {
val activity = LocalActivity.current
val walletViewModel by activity.viewModels<WalletViewModel>()
val walletViewModel = koinActivityViewModel<WalletViewModel>()
val createTransactionsViewModel by activity.viewModels<CreateTransactionsViewModel>()
val createTransactionsViewModel = koinActivityViewModel<CreateTransactionsViewModel>()
val homeViewModel by activity.viewModels<HomeViewModel>()
val homeViewModel = koinActivityViewModel<HomeViewModel>()
val synchronizer = walletViewModel.synchronizer.collectAsStateWithLifecycle().value
@ -69,12 +68,7 @@ internal fun WrapBalances(
val isHideBalances = homeViewModel.isHideBalances.collectAsStateWithLifecycle().value ?: false
val checkUpdateViewModel by activity.viewModels<CheckUpdateViewModel> {
CheckUpdateViewModel.CheckUpdateViewModelFactory(
activity.application,
AppUpdateCheckerImp.new()
)
}
val checkUpdateViewModel = koinActivityViewModel<CheckUpdateViewModel>()
val balanceState = walletViewModel.balanceState.collectAsStateWithLifecycle().value

View File

@ -3,7 +3,6 @@
package co.electriccoin.zcash.ui.screen.chooseserver
import androidx.activity.compose.BackHandler
import androidx.activity.viewModels
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
@ -18,6 +17,7 @@ import cash.z.ecc.android.sdk.model.PersistableWallet
import cash.z.ecc.android.sdk.model.ZcashNetwork
import cash.z.ecc.android.sdk.type.ServerValidation
import cash.z.ecc.sdk.type.fromResources
import co.electriccoin.zcash.di.koinActivityViewModel
import co.electriccoin.zcash.ui.MainActivity
import co.electriccoin.zcash.ui.common.model.TopAppBarSubTitleState
import co.electriccoin.zcash.ui.common.viewmodel.SecretState
@ -28,7 +28,7 @@ import kotlinx.coroutines.launch
@Composable
internal fun MainActivity.WrapChooseServer(goBack: () -> Unit) {
val walletViewModel by viewModels<WalletViewModel>()
val walletViewModel = koinActivityViewModel<WalletViewModel>()
val secretState = walletViewModel.secretState.collectAsStateWithLifecycle().value

View File

@ -2,12 +2,12 @@ package co.electriccoin.zcash.ui.screen.deletewallet
import android.app.Activity
import androidx.activity.compose.BackHandler
import androidx.activity.viewModels
import androidx.compose.material3.SnackbarHostState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import co.electriccoin.zcash.di.koinActivityViewModel
import co.electriccoin.zcash.spackle.Twig
import co.electriccoin.zcash.ui.MainActivity
import co.electriccoin.zcash.ui.R
@ -18,7 +18,7 @@ import kotlinx.coroutines.launch
@Composable
internal fun MainActivity.WrapDeleteWallet(goBack: () -> Unit) {
val walletViewModel by viewModels<WalletViewModel>()
val walletViewModel = koinActivityViewModel<WalletViewModel>()
val walletState = walletViewModel.walletStateInformation.collectAsStateWithLifecycle().value

View File

@ -2,7 +2,6 @@ package co.electriccoin.zcash.ui.screen.exportdata
import android.content.Context
import androidx.activity.compose.BackHandler
import androidx.activity.viewModels
import androidx.compose.material3.SnackbarHostState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
@ -12,7 +11,7 @@ import cash.z.ecc.android.sdk.SdkSynchronizer
import cash.z.ecc.android.sdk.Synchronizer
import cash.z.ecc.android.sdk.model.ZcashNetwork
import cash.z.ecc.sdk.type.fromResources
import co.electriccoin.zcash.ui.MainActivity
import co.electriccoin.zcash.di.koinActivityViewModel
import co.electriccoin.zcash.ui.R
import co.electriccoin.zcash.ui.common.compose.LocalActivity
import co.electriccoin.zcash.ui.common.model.TopAppBarSubTitleState
@ -27,11 +26,11 @@ import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.launch
@Composable
internal fun MainActivity.WrapExportPrivateData(
internal fun WrapExportPrivateData(
goBack: () -> Unit,
onConfirm: () -> Unit
) {
val walletViewModel by viewModels<WalletViewModel>()
val walletViewModel = koinActivityViewModel<WalletViewModel>()
val synchronizer = walletViewModel.synchronizer.collectAsStateWithLifecycle().value

View File

@ -3,7 +3,6 @@
package co.electriccoin.zcash.ui.screen.home
import androidx.activity.compose.BackHandler
import androidx.activity.viewModels
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.runtime.Composable
@ -19,6 +18,7 @@ import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.res.stringResource
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import cash.z.ecc.android.sdk.model.ZecSend
import co.electriccoin.zcash.di.koinActivityViewModel
import co.electriccoin.zcash.ui.R
import co.electriccoin.zcash.ui.common.compose.LocalActivity
import co.electriccoin.zcash.ui.common.compose.RestoreScreenBrightness
@ -47,11 +47,9 @@ internal fun WrapHome(
goSendConfirmation: (ZecSend) -> Unit,
sendArguments: SendArguments
) {
val activity = LocalActivity.current
val homeViewModel = koinActivityViewModel<HomeViewModel>()
val homeViewModel by activity.viewModels<HomeViewModel>()
val walletViewModel by activity.viewModels<WalletViewModel>()
val walletViewModel = koinActivityViewModel<WalletViewModel>()
val isKeepScreenOnWhileSyncing = homeViewModel.isKeepScreenOnWhileSyncing.collectAsStateWithLifecycle().value
@ -117,7 +115,7 @@ internal fun WrapHome(
val focusManager = LocalFocusManager.current
val walletViewModel by activity.viewModels<WalletViewModel>()
val walletViewModel = koinActivityViewModel<WalletViewModel>()
val scope = rememberCoroutineScope()

View File

@ -3,7 +3,6 @@
package co.electriccoin.zcash.ui.screen.onboarding
import android.content.Context
import androidx.activity.viewModels
import androidx.compose.runtime.Composable
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import cash.z.ecc.android.sdk.WalletInitMode
@ -13,6 +12,7 @@ import cash.z.ecc.android.sdk.model.PersistableWallet
import cash.z.ecc.android.sdk.model.SeedPhrase
import cash.z.ecc.android.sdk.model.ZcashNetwork
import cash.z.ecc.sdk.type.fromResources
import co.electriccoin.zcash.di.koinActivityViewModel
import co.electriccoin.zcash.spackle.FirebaseTestLabUtil
import co.electriccoin.zcash.ui.common.compose.LocalActivity
import co.electriccoin.zcash.ui.common.model.OnboardingState
@ -28,8 +28,8 @@ import co.electriccoin.zcash.ui.screen.restore.WrapRestore
@Composable
internal fun WrapOnboarding() {
val activity = LocalActivity.current
val walletViewModel by activity.viewModels<WalletViewModel>()
val onboardingViewModel by activity.viewModels<OnboardingViewModel>()
val walletViewModel = koinActivityViewModel<WalletViewModel>()
val onboardingViewModel = koinActivityViewModel<OnboardingViewModel>()
val versionInfo = VersionInfo.new(activity.applicationContext)
@ -75,7 +75,7 @@ internal fun WrapOnboarding() {
activity.reportFullyDrawn()
} else {
WrapRestore(activity)
WrapRestore()
}
}

View File

@ -4,13 +4,13 @@ package co.electriccoin.zcash.ui.screen.receive
import android.content.Context
import android.graphics.Bitmap
import androidx.activity.viewModels
import androidx.compose.material3.SnackbarHostState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.graphics.asAndroidBitmap
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import co.electriccoin.zcash.di.koinActivityViewModel
import co.electriccoin.zcash.spackle.ClipboardManagerUtil
import co.electriccoin.zcash.spackle.Twig
import co.electriccoin.zcash.spackle.getInternalCacheDirSuspend
@ -34,9 +34,9 @@ import java.io.File
internal fun WrapReceive(onSettings: () -> Unit) {
val activity = LocalActivity.current
val walletViewModel by activity.viewModels<WalletViewModel>()
val walletViewModel = koinActivityViewModel<WalletViewModel>()
val brightnessViewModel by activity.viewModels<ScreenBrightnessViewModel>()
val brightnessViewModel = koinActivityViewModel<ScreenBrightnessViewModel>()
val screenBrightnessState = brightnessViewModel.screenBrightnessState.collectAsStateWithLifecycle().value

View File

@ -1,14 +1,13 @@
package co.electriccoin.zcash.ui.screen.restore
import android.content.ClipboardManager
import androidx.activity.ComponentActivity
import androidx.activity.viewModels
import androidx.compose.runtime.Composable
import androidx.compose.ui.platform.LocalContext
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import cash.z.ecc.android.sdk.model.SeedPhrase
import cash.z.ecc.android.sdk.model.ZcashNetwork
import cash.z.ecc.sdk.type.fromResources
import co.electriccoin.zcash.di.koinActivityViewModel
import co.electriccoin.zcash.ui.common.viewmodel.WalletViewModel
import co.electriccoin.zcash.ui.screen.onboarding.persistExistingWalletWithSeedPhrase
import co.electriccoin.zcash.ui.screen.onboarding.viewmodel.OnboardingViewModel
@ -17,10 +16,10 @@ import co.electriccoin.zcash.ui.screen.restore.viewmodel.CompleteWordSetState
import co.electriccoin.zcash.ui.screen.restore.viewmodel.RestoreViewModel
@Composable
fun WrapRestore(activity: ComponentActivity) {
val walletViewModel by activity.viewModels<WalletViewModel>()
val onboardingViewModel by activity.viewModels<OnboardingViewModel>()
val restoreViewModel by activity.viewModels<RestoreViewModel>()
fun WrapRestore() {
val walletViewModel = koinActivityViewModel<WalletViewModel>()
val onboardingViewModel = koinActivityViewModel<OnboardingViewModel>()
val restoreViewModel = koinActivityViewModel<RestoreViewModel>()
val applicationContext = LocalContext.current.applicationContext

View File

@ -8,8 +8,8 @@ import cash.z.ecc.android.bip39.Mnemonics
import cash.z.ecc.android.sdk.ext.collectWith
import cash.z.ecc.android.sdk.model.BlockHeight
import cash.z.ecc.android.sdk.model.ZcashNetwork
import cash.z.ecc.sdk.ANDROID_STATE_FLOW_TIMEOUT
import cash.z.ecc.sdk.type.fromResources
import co.electriccoin.zcash.ui.common.ANDROID_STATE_FLOW_TIMEOUT
import co.electriccoin.zcash.ui.screen.restore.model.RestoreStage
import co.electriccoin.zcash.ui.screen.restore.state.RestoreState
import co.electriccoin.zcash.ui.screen.restore.state.WordList

View File

@ -1,17 +1,14 @@
package co.electriccoin.zcash.ui.screen.restoresuccess
import androidx.activity.viewModels
import androidx.compose.runtime.Composable
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import co.electriccoin.zcash.ui.common.compose.LocalActivity
import co.electriccoin.zcash.di.koinActivityViewModel
import co.electriccoin.zcash.ui.screen.restoresuccess.view.RestoreSuccess
import co.electriccoin.zcash.ui.screen.restoresuccess.viewmodel.RestoreSuccessViewModel
@Composable
fun WrapRestoreSuccess(onDone: () -> Unit) {
val activity = LocalActivity.current
val viewModel by activity.viewModels<RestoreSuccessViewModel>()
val viewModel = koinActivityViewModel<RestoreSuccessViewModel>()
val state = viewModel.state.collectAsStateWithLifecycle().value

View File

@ -3,10 +3,10 @@ package co.electriccoin.zcash.ui.screen.restoresuccess.viewmodel
import android.app.Application
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.viewModelScope
import cash.z.ecc.sdk.ANDROID_STATE_FLOW_TIMEOUT
import co.electriccoin.zcash.preference.StandardPreferenceProvider
import co.electriccoin.zcash.preference.model.entry.BooleanPreferenceDefault
import co.electriccoin.zcash.ui.common.ANDROID_STATE_FLOW_TIMEOUT
import co.electriccoin.zcash.ui.preference.StandardPreferenceKeys
import co.electriccoin.zcash.ui.preference.StandardPreferenceSingleton
import co.electriccoin.zcash.ui.screen.restoresuccess.view.RestoreSuccessViewState
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
@ -16,7 +16,10 @@ import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
class RestoreSuccessViewModel(application: Application) : AndroidViewModel(application) {
class RestoreSuccessViewModel(
application: Application,
private val standardPreferenceProvider: StandardPreferenceProvider,
) : AndroidViewModel(application) {
private val keepScreenOn = MutableStateFlow(DEFAULT_KEEP_SCREEN_ON)
val state =
@ -45,8 +48,7 @@ class RestoreSuccessViewModel(application: Application) : AndroidViewModel(appli
default: BooleanPreferenceDefault,
newState: Boolean
) = viewModelScope.launch {
val prefs = StandardPreferenceSingleton.getInstance(getApplication())
default.putValue(prefs, newState)
default.putValue(standardPreferenceProvider(), newState)
}
}

View File

@ -1,7 +1,6 @@
package co.electriccoin.zcash.ui.screen.scan
import androidx.activity.compose.BackHandler
import androidx.activity.viewModels
import androidx.compose.material3.SnackbarHostState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
@ -13,6 +12,7 @@ import androidx.compose.ui.platform.LocalContext
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import cash.z.ecc.android.sdk.Synchronizer
import cash.z.ecc.android.sdk.type.AddressType
import co.electriccoin.zcash.di.koinActivityViewModel
import co.electriccoin.zcash.ui.MainActivity
import co.electriccoin.zcash.ui.R
import co.electriccoin.zcash.ui.common.model.SerializableAddress
@ -28,7 +28,7 @@ internal fun MainActivity.WrapScanValidator(
onScanValid: (address: SerializableAddress) -> Unit,
goBack: () -> Unit
) {
val walletViewModel by viewModels<WalletViewModel>()
val walletViewModel = koinActivityViewModel<WalletViewModel>()
val synchronizer = walletViewModel.synchronizer.collectAsStateWithLifecycle().value

View File

@ -3,10 +3,11 @@ package co.electriccoin.zcash.ui.screen.securitywarning
import androidx.activity.compose.BackHandler
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import co.electriccoin.zcash.configuration.AndroidConfigurationFactory
import co.electriccoin.zcash.configuration.api.ConfigurationProvider
import co.electriccoin.zcash.ui.common.compose.LocalActivity
import co.electriccoin.zcash.ui.common.model.VersionInfo
import co.electriccoin.zcash.ui.screen.securitywarning.view.SecurityWarning
import org.koin.compose.koinInject
@Composable
internal fun WrapSecurityWarning(
@ -14,7 +15,7 @@ internal fun WrapSecurityWarning(
onConfirm: () -> Unit
) {
val activity = LocalActivity.current
val androidConfigurationProvider = koinInject<ConfigurationProvider>()
BackHandler {
onBack()
}
@ -29,6 +30,6 @@ internal fun WrapSecurityWarning(
)
LaunchedEffect(key1 = true) {
AndroidConfigurationFactory.getInstance(activity.applicationContext).hintToRefresh()
androidConfigurationProvider.hintToRefresh()
}
}

View File

@ -1,10 +1,10 @@
package co.electriccoin.zcash.ui.screen.seedrecovery
import androidx.activity.compose.BackHandler
import androidx.activity.viewModels
import androidx.compose.runtime.Composable
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import cash.z.ecc.android.sdk.Synchronizer
import co.electriccoin.zcash.di.koinActivityViewModel
import co.electriccoin.zcash.spackle.ClipboardManagerUtil
import co.electriccoin.zcash.ui.R
import co.electriccoin.zcash.ui.common.compose.LocalActivity
@ -20,9 +20,7 @@ internal fun WrapSeedRecovery(
goBack: () -> Unit,
onDone: () -> Unit,
) {
val activity = LocalActivity.current
val walletViewModel by activity.viewModels<WalletViewModel>()
val walletViewModel = koinActivityViewModel<WalletViewModel>()
val synchronizer = walletViewModel.synchronizer.collectAsStateWithLifecycle().value

View File

@ -3,7 +3,6 @@
package co.electriccoin.zcash.ui.screen.send
import android.content.pm.PackageManager
import androidx.activity.viewModels
import androidx.annotation.VisibleForTesting
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
@ -19,6 +18,7 @@ import cash.z.ecc.android.sdk.model.ZecSend
import cash.z.ecc.android.sdk.model.proposeSend
import cash.z.ecc.android.sdk.model.toZecString
import cash.z.ecc.android.sdk.type.AddressType
import co.electriccoin.zcash.di.koinActivityViewModel
import co.electriccoin.zcash.spackle.Twig
import co.electriccoin.zcash.ui.common.compose.BalanceState
import co.electriccoin.zcash.ui.common.compose.LocalActivity
@ -50,9 +50,9 @@ internal fun WrapSend(
) {
val activity = LocalActivity.current
val walletViewModel by activity.viewModels<WalletViewModel>()
val walletViewModel = koinActivityViewModel<WalletViewModel>()
val homeViewModel by activity.viewModels<HomeViewModel>()
val homeViewModel = koinActivityViewModel<HomeViewModel>()
val hasCameraFeature = activity.packageManager.hasSystemFeature(PackageManager.FEATURE_CAMERA_ANY)

View File

@ -5,7 +5,6 @@ package co.electriccoin.zcash.ui.screen.sendconfirmation
import android.content.Context
import android.content.Intent
import androidx.activity.compose.BackHandler
import androidx.activity.viewModels
import androidx.annotation.VisibleForTesting
import androidx.compose.material3.SnackbarHostState
import androidx.compose.runtime.Composable
@ -23,6 +22,7 @@ import cash.z.ecc.android.sdk.model.Proposal
import cash.z.ecc.android.sdk.model.TransactionSubmitResult
import cash.z.ecc.android.sdk.model.UnifiedSpendingKey
import cash.z.ecc.android.sdk.model.ZecSend
import co.electriccoin.zcash.di.koinActivityViewModel
import co.electriccoin.zcash.spackle.Twig
import co.electriccoin.zcash.ui.MainActivity
import co.electriccoin.zcash.ui.R
@ -57,15 +57,13 @@ internal fun MainActivity.WrapSendConfirmation(
goSupport: () -> Unit,
arguments: SendConfirmationArguments
) {
val walletViewModel by viewModels<WalletViewModel>()
val walletViewModel = koinActivityViewModel<WalletViewModel>()
val createTransactionsViewModel by viewModels<CreateTransactionsViewModel>()
val createTransactionsViewModel = koinActivityViewModel<CreateTransactionsViewModel>()
val supportViewModel by viewModels<SupportViewModel>()
val supportViewModel = koinActivityViewModel<SupportViewModel>()
val authenticationViewModel by viewModels<AuthenticationViewModel> {
AuthenticationViewModel.AuthenticationViewModelFactory(application)
}
val authenticationViewModel = koinActivityViewModel<AuthenticationViewModel>()
val synchronizer = walletViewModel.synchronizer.collectAsStateWithLifecycle().value

View File

@ -1,7 +1,6 @@
package co.electriccoin.zcash.ui.screen.sendconfirmation.viewmodel
import android.app.Application
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.ViewModel
import cash.z.ecc.android.sdk.Synchronizer
import cash.z.ecc.android.sdk.model.Proposal
import cash.z.ecc.android.sdk.model.TransactionSubmitResult
@ -10,7 +9,7 @@ import co.electriccoin.zcash.spackle.Twig
import co.electriccoin.zcash.ui.screen.sendconfirmation.model.SubmitResult
import kotlinx.coroutines.flow.MutableStateFlow
class CreateTransactionsViewModel(application: Application) : AndroidViewModel(application) {
class CreateTransactionsViewModel : ViewModel() {
// Technically this value will not survive process dead, but will survive all possible configuration changes
// Possible solution would be storing the value within [SavedStateHandle]
val submissions: MutableStateFlow<List<TransactionSubmitResult>> = MutableStateFlow(emptyList())

View File

@ -1,9 +1,9 @@
package co.electriccoin.zcash.ui.screen.settings
import androidx.activity.compose.BackHandler
import androidx.activity.viewModels
import androidx.compose.runtime.Composable
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import co.electriccoin.zcash.di.koinActivityViewModel
import co.electriccoin.zcash.ui.common.compose.LocalActivity
import co.electriccoin.zcash.ui.common.model.VersionInfo
import co.electriccoin.zcash.ui.common.viewmodel.WalletViewModel
@ -23,9 +23,9 @@ internal fun WrapSettings(
) {
val activity = LocalActivity.current
val walletViewModel by activity.viewModels<WalletViewModel>()
val walletViewModel = koinActivityViewModel<WalletViewModel>()
val settingsViewModel by activity.viewModels<SettingsViewModel>()
val settingsViewModel = koinActivityViewModel<SettingsViewModel>()
val walletState = walletViewModel.walletStateInformation.collectAsStateWithLifecycle().value

View File

@ -1,12 +1,11 @@
package co.electriccoin.zcash.ui.screen.settings.viewmodel
import android.app.Application
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import cash.z.ecc.sdk.ANDROID_STATE_FLOW_TIMEOUT
import co.electriccoin.zcash.preference.StandardPreferenceProvider
import co.electriccoin.zcash.preference.model.entry.BooleanPreferenceDefault
import co.electriccoin.zcash.ui.common.ANDROID_STATE_FLOW_TIMEOUT
import co.electriccoin.zcash.ui.preference.StandardPreferenceKeys
import co.electriccoin.zcash.ui.preference.StandardPreferenceSingleton
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.WhileSubscribed
@ -15,7 +14,9 @@ import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
class SettingsViewModel(application: Application) : AndroidViewModel(application) {
class SettingsViewModel(
private val standardPreferenceProvider: StandardPreferenceProvider,
) : ViewModel() {
val isAnalyticsEnabled: StateFlow<Boolean?> = booleanStateFlow(StandardPreferenceKeys.IS_ANALYTICS_ENABLED)
val isBackgroundSync: StateFlow<Boolean?> = booleanStateFlow(StandardPreferenceKeys.IS_BACKGROUND_SYNC_ENABLED)
@ -25,8 +26,7 @@ class SettingsViewModel(application: Application) : AndroidViewModel(application
private fun booleanStateFlow(default: BooleanPreferenceDefault): StateFlow<Boolean?> =
flow<Boolean?> {
val preferenceProvider = StandardPreferenceSingleton.getInstance(getApplication())
emitAll(default.observe(preferenceProvider))
emitAll(default.observe(standardPreferenceProvider()))
}.stateIn(viewModelScope, SharingStarted.WhileSubscribed(ANDROID_STATE_FLOW_TIMEOUT), null)
fun setAnalyticsEnabled(enabled: Boolean) {
@ -46,8 +46,7 @@ class SettingsViewModel(application: Application) : AndroidViewModel(application
newState: Boolean
) {
viewModelScope.launch {
val prefs = StandardPreferenceSingleton.getInstance(getApplication())
default.putValue(prefs, newState)
default.putValue(standardPreferenceProvider(), newState)
}
}
}

View File

@ -4,7 +4,6 @@ package co.electriccoin.zcash.ui.screen.support
import android.content.Intent
import androidx.activity.compose.BackHandler
import androidx.activity.viewModels
import androidx.annotation.VisibleForTesting
import androidx.compose.material3.SnackbarHostState
import androidx.compose.runtime.Composable
@ -13,6 +12,7 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import co.electriccoin.zcash.di.koinActivityViewModel
import co.electriccoin.zcash.ui.R
import co.electriccoin.zcash.ui.common.compose.LocalActivity
import co.electriccoin.zcash.ui.common.model.TopAppBarSubTitleState
@ -26,11 +26,9 @@ import kotlinx.coroutines.launch
@Composable
internal fun WrapSupport(goBack: () -> Unit) {
val activity = LocalActivity.current
val supportViewModel = koinActivityViewModel<SupportViewModel>()
val supportViewModel by activity.viewModels<SupportViewModel>()
val walletViewModel by activity.viewModels<WalletViewModel>()
val walletViewModel = koinActivityViewModel<WalletViewModel>()
val supportInfo = supportViewModel.supportInfo.collectAsStateWithLifecycle().value

View File

@ -1,7 +1,7 @@
package co.electriccoin.zcash.ui.screen.support.model
import android.content.Context
import co.electriccoin.zcash.configuration.AndroidConfigurationFactory
import co.electriccoin.zcash.configuration.api.ConfigurationProvider
import co.electriccoin.zcash.spackle.getPackageInfoCompatSuspend
import kotlinx.collections.immutable.PersistentList
import kotlinx.collections.immutable.toPersistentList
@ -63,15 +63,17 @@ data class SupportInfo(
companion object {
// Although most of our calls now are non-blocking, we expect more of them to be blocking
// in the future.
suspend fun new(context: Context): SupportInfo {
suspend fun new(
context: Context,
androidConfigurationProvider: ConfigurationProvider
): 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),
ConfigInfo.new(androidConfigurationProvider),
OperatingSystemInfo.new(),
DeviceInfo.new(),
EnvironmentInfo.new(applicationContext),

View File

@ -3,7 +3,8 @@ package co.electriccoin.zcash.ui.screen.support.viewmodel
import android.app.Application
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.viewModelScope
import co.electriccoin.zcash.ui.common.ANDROID_STATE_FLOW_TIMEOUT
import cash.z.ecc.sdk.ANDROID_STATE_FLOW_TIMEOUT
import co.electriccoin.zcash.configuration.api.ConfigurationProvider
import co.electriccoin.zcash.ui.screen.support.model.SupportInfo
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
@ -12,13 +13,16 @@ import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.stateIn
import kotlin.time.Duration
class SupportViewModel(application: Application) : AndroidViewModel(application) {
class SupportViewModel(
application: Application,
androidConfigurationProvider: ConfigurationProvider
) : AndroidViewModel(application) {
// Technically, some of the support info could be invalidated after a configuration change,
// such as the user's current locale. However it really doesn't matter here since all we
// care about is capturing a snapshot of the app, OS, and device state.
val supportInfo: StateFlow<SupportInfo?> =
flow<SupportInfo?> {
emit(SupportInfo.new(application))
emit(SupportInfo.new(application, androidConfigurationProvider))
}
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(ANDROID_STATE_FLOW_TIMEOUT, Duration.ZERO), null)
}

View File

@ -2,7 +2,6 @@ package co.electriccoin.zcash.ui.screen.update
import android.content.Context
import androidx.activity.compose.BackHandler
import androidx.activity.viewModels
import androidx.annotation.VisibleForTesting
import androidx.compose.material3.SnackbarHostState
import androidx.compose.runtime.Composable
@ -10,6 +9,7 @@ import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import co.electriccoin.zcash.di.koinActivityViewModel
import co.electriccoin.zcash.ui.R
import co.electriccoin.zcash.ui.common.compose.LocalActivity
import co.electriccoin.zcash.ui.common.viewmodel.CheckUpdateViewModel
@ -20,27 +20,34 @@ import co.electriccoin.zcash.ui.screen.update.viewmodel.UpdateViewModel
import co.electriccoin.zcash.ui.util.PlayStoreUtil
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import org.koin.core.parameter.parametersOf
@Composable
internal fun WrapCheckForUpdate() {
val activity = LocalActivity.current
// TODO [#403]: Manual testing of already implemented in-app update mechanisms
// TODO [#403]: https://github.com/Electric-Coin-Company/zashi-android/issues/403
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
val checkUpdateViewModel by activity.viewModels<CheckUpdateViewModel> {
CheckUpdateViewModel.CheckUpdateViewModelFactory(
activity.application,
AppUpdateCheckerImp.new()
val checkUpdateViewModel = koinActivityViewModel<CheckUpdateViewModel>()
val activity = LocalActivity.current
val inputUpdateInfo = checkUpdateViewModel.updateInfo.collectAsStateWithLifecycle().value ?: return
val viewModel = koinActivityViewModel<UpdateViewModel> { parametersOf(inputUpdateInfo) }
val updateInfo = viewModel.updateInfo.collectAsStateWithLifecycle().value
if (updateInfo.appUpdateInfo != null && updateInfo.state == UpdateState.Prepared) {
WrapUpdate(
updateInfo = updateInfo,
checkForUpdate = viewModel::checkForAppUpdate,
remindLater = viewModel::remindLater,
goForUpdate = {
viewModel.goForUpdate(
activity = activity,
appUpdateInfo = updateInfo.appUpdateInfo
)
}
val updateInfo = checkUpdateViewModel.updateInfo.collectAsStateWithLifecycle().value
updateInfo?.let {
if (it.appUpdateInfo != null && it.state == UpdateState.Prepared) {
WrapUpdate(updateInfo)
}
)
}
// Check for an app update asynchronously. We create an effect that matches the activity
@ -52,22 +59,17 @@ internal fun WrapCheckForUpdate() {
@VisibleForTesting
@Composable
internal fun WrapUpdate(inputUpdateInfo: UpdateInfo) {
internal fun WrapUpdate(
updateInfo: UpdateInfo,
checkForUpdate: () -> Unit,
remindLater: () -> Unit,
goForUpdate: () -> Unit,
) {
val activity = LocalActivity.current
val viewModel by activity.viewModels<UpdateViewModel> {
UpdateViewModel.UpdateViewModelFactory(
activity.application,
inputUpdateInfo,
AppUpdateCheckerImp.new()
)
}
val snackbarHostState = remember { SnackbarHostState() }
val scope = rememberCoroutineScope()
val updateInfo = viewModel.updateInfo.collectAsStateWithLifecycle().value
when (updateInfo.state) {
UpdateState.Done, UpdateState.Canceled -> {
// just return as we are already in Home compose
@ -76,7 +78,7 @@ internal fun WrapUpdate(inputUpdateInfo: UpdateInfo) {
UpdateState.Failed -> {
// we need to refresh AppUpdateInfo object, as it can be used only once
viewModel.checkForAppUpdate()
checkForUpdate()
}
UpdateState.Prepared, UpdateState.Running -> {
@ -86,7 +88,7 @@ internal fun WrapUpdate(inputUpdateInfo: UpdateInfo) {
val onLaterAction = {
if (!updateInfo.isForce && updateInfo.state != UpdateState.Running) {
viewModel.remindLater()
remindLater()
}
}
@ -100,11 +102,7 @@ internal fun WrapUpdate(inputUpdateInfo: UpdateInfo) {
onDownload = {
// in this state of the update we have the AppUpdateInfo filled
requireNotNull(updateInfo.appUpdateInfo)
viewModel.goForUpdate(
activity,
updateInfo.appUpdateInfo
)
goForUpdate()
},
onLater = onLaterAction,
onReference = {

View File

@ -15,13 +15,7 @@ import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.callbackFlow
class AppUpdateCheckerImp private constructor() : AppUpdateChecker {
companion object {
private const val DEFAULT_STALENESS_DAYS = 3
fun new() = AppUpdateCheckerImp()
}
class AppUpdateCheckerImpl : AppUpdateChecker {
override val stalenessDays = DEFAULT_STALENESS_DAYS
/**
@ -133,3 +127,5 @@ class AppUpdateCheckerImp private constructor() : AppUpdateChecker {
}
}
}
private const val DEFAULT_STALENESS_DAYS = 3

View File

@ -4,8 +4,6 @@ import android.app.Activity
import android.app.Application
import androidx.activity.ComponentActivity
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewModelScope
import cash.z.ecc.android.sdk.ext.onFirst
import co.electriccoin.zcash.ui.screen.update.AppUpdateChecker
@ -14,6 +12,7 @@ import co.electriccoin.zcash.ui.screen.update.model.UpdateState
import com.google.android.play.core.appupdate.AppUpdateInfo
import com.google.android.play.core.install.model.ActivityResult
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
class UpdateViewModel(
@ -62,21 +61,6 @@ class UpdateViewModel(
fun remindLater() {
// for mvp we just return user back to the previous screen
updateInfo.value = updateInfo.value.copy(state = UpdateState.Canceled)
}
@Suppress("UNCHECKED_CAST")
class UpdateViewModelFactory(
private val application: Application,
private val updateInfo: UpdateInfo,
private val appUpdateChecker: AppUpdateChecker
) : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
return if (modelClass.isAssignableFrom(UpdateViewModel::class.java)) {
UpdateViewModel(application, updateInfo, appUpdateChecker) as T
} else {
throw IllegalArgumentException("ViewModel Not Found.")
}
}
updateInfo.update { it.copy(state = UpdateState.Canceled) }
}
}

View File

@ -3,13 +3,13 @@
package co.electriccoin.zcash.ui.screen.warning
import androidx.activity.compose.BackHandler
import androidx.activity.viewModels
import androidx.compose.material3.SnackbarHostState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.platform.LocalContext
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import co.electriccoin.zcash.di.koinActivityViewModel
import co.electriccoin.zcash.ui.MainActivity
import co.electriccoin.zcash.ui.R
import co.electriccoin.zcash.ui.common.model.TopAppBarSubTitleState
@ -24,9 +24,9 @@ fun MainActivity.WrapNotEnoughSpace(
goPrevious: () -> Unit,
goSettings: () -> Unit
) {
val walletViewModel by viewModels<WalletViewModel>()
val walletViewModel = koinActivityViewModel<WalletViewModel>()
val storageCheckViewModel by viewModels<StorageCheckViewModel>()
val storageCheckViewModel = koinActivityViewModel<StorageCheckViewModel>()
val walletState = walletViewModel.walletStateInformation.collectAsStateWithLifecycle().value

View File

@ -2,8 +2,8 @@ package co.electriccoin.zcash.ui.screen.warning.viewmodel
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import cash.z.ecc.sdk.ANDROID_STATE_FLOW_TIMEOUT
import co.electriccoin.zcash.global.StorageChecker
import co.electriccoin.zcash.ui.common.ANDROID_STATE_FLOW_TIMEOUT
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.WhileSubscribed
import kotlinx.coroutines.flow.flow

View File

@ -1,11 +1,10 @@
package co.electriccoin.zcash.ui.screen.whatsnew
import androidx.activity.compose.BackHandler
import androidx.activity.viewModels
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import co.electriccoin.zcash.ui.common.compose.LocalActivity
import co.electriccoin.zcash.di.koinActivityViewModel
import co.electriccoin.zcash.ui.common.compose.LocalNavController
import co.electriccoin.zcash.ui.common.viewmodel.WalletViewModel
import co.electriccoin.zcash.ui.screen.whatsnew.view.WhatsNewView
@ -13,10 +12,9 @@ import co.electriccoin.zcash.ui.screen.whatsnew.viewmodel.WhatsNewViewModel
@Composable
fun WrapWhatsNew() {
val activity = LocalActivity.current
val navController = LocalNavController.current
val viewModel by activity.viewModels<WhatsNewViewModel>()
val walletViewModel by activity.viewModels<WalletViewModel>()
val viewModel = koinActivityViewModel<WhatsNewViewModel>()
val walletViewModel = koinActivityViewModel<WalletViewModel>()
val walletState by walletViewModel.walletStateInformation.collectAsStateWithLifecycle()
val state = viewModel.state.collectAsStateWithLifecycle().value ?: return

View File

@ -3,7 +3,7 @@ package co.electriccoin.zcash.ui.screen.whatsnew.viewmodel
import android.app.Application
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.viewModelScope
import co.electriccoin.zcash.ui.common.ANDROID_STATE_FLOW_TIMEOUT
import cash.z.ecc.sdk.ANDROID_STATE_FLOW_TIMEOUT
import co.electriccoin.zcash.ui.common.model.VersionInfo
import co.electriccoin.zcash.ui.screen.whatsnew.model.WhatsNewState
import kotlinx.coroutines.flow.SharingStarted

View File

@ -9,10 +9,9 @@ import androidx.work.PeriodicWorkRequest
import androidx.work.PeriodicWorkRequestBuilder
import androidx.work.WorkerParameters
import cash.z.ecc.android.sdk.Synchronizer
import cash.z.ecc.android.sdk.WalletCoordinator
import cash.z.ecc.android.sdk.model.PercentDecimal
import co.electriccoin.zcash.global.getInstance
import co.electriccoin.zcash.spackle.Twig
import co.electriccoin.zcash.ui.common.usecase.ObserveSynchronizerUseCase
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.combine
@ -27,6 +26,8 @@ import kotlinx.datetime.atTime
import kotlinx.datetime.toInstant
import kotlinx.datetime.toLocalDateTime
import kotlinx.datetime.until
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
import kotlin.random.Random
import kotlin.time.Duration
import kotlin.time.Duration.Companion.days
@ -38,14 +39,18 @@ import kotlin.time.toJavaDuration
// TODO [#1249]: Add documentation and tests on background syncing
// TODO [#1249]: https://github.com/Electric-Coin-Company/zashi-android/issues/1249
@Keep
class SyncWorker(context: Context, workerParameters: WorkerParameters) : CoroutineWorker(context, workerParameters) {
class SyncWorker(
context: Context,
workerParameters: WorkerParameters
) : CoroutineWorker(context, workerParameters), KoinComponent {
private val observeSynchronizer: ObserveSynchronizerUseCase by inject()
@OptIn(ExperimentalCoroutinesApi::class)
override suspend fun doWork(): Result {
Twig.debug { "BG Sync: starting..." }
WalletCoordinator.getInstance(applicationContext).synchronizer
observeSynchronizer()
.flatMapLatest {
Twig.debug { "BG Sync: synchronizer: $it" }

View File

@ -21,7 +21,7 @@ android {
testInstrumentationRunnerArguments["clearPackageData"] = "true"
}
testInstrumentationRunner = "co.electriccoin.zcash.test.ZcashUiTestRunner"
testInstrumentationRunner = "co.electroniccoin.zcash.ui.screenshot.ZcashScreenshotTestRunner"
}
// Define the same flavors as in app module

View File

@ -0,0 +1,38 @@
package co.electroniccoin.zcash.ui.screenshot
import android.app.Application
import android.content.Context
import co.electriccoin.zcash.di.coreModule
import co.electriccoin.zcash.di.repositoryModule
import co.electriccoin.zcash.di.useCaseModule
import co.electriccoin.zcash.di.viewModelModule
import co.electriccoin.zcash.test.ZcashUiTestRunner
import org.koin.android.ext.koin.androidContext
import org.koin.android.ext.koin.androidLogger
import org.koin.core.context.startKoin
class ZcashScreenshotTestRunner : ZcashUiTestRunner() {
override fun newApplication(
cl: ClassLoader?,
className: String?,
context: Context?
): Application {
return super.newApplication(cl, ZcashUiTestApplication::class.java.name, context)
}
}
class ZcashUiTestApplication : Application() {
override fun onCreate() {
super.onCreate()
startKoin {
androidLogger()
androidContext(this@ZcashUiTestApplication)
modules(
coreModule,
repositoryModule,
useCaseModule,
viewModelModule
)
}
}
}