Merge pull request #264 from zcash/fix/refactor-startup

Fix/refactor startup
This commit is contained in:
Kevin Gorham 2021-05-02 00:28:33 -04:00 committed by GitHub
commit 3a45ac5db2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 78 additions and 62 deletions

View File

@ -31,9 +31,6 @@
android:resource="@xml/file_paths" />
</provider>
<!-- Firebase options -->
<meta-data android:name="com.google.firebase.ml.vision.DEPENDENCIES" android:value="barcode" />
<!-- Mixpanel options -->
<meta-data android:name="com.mixpanel.android.MPConfig.AutoShowMixpanelUpdates" android:value="false" />
<meta-data android:name="com.mixpanel.android.MPConfig.EnableDebugLogging" android:value="false" />

View File

@ -81,11 +81,8 @@ import cash.z.ecc.android.ui.history.HistoryViewModel
import cash.z.ecc.android.ui.util.MemoUtil
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.snackbar.Snackbar
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch
import java.lang.RuntimeException
import javax.inject.Inject
@ -265,6 +262,7 @@ class MainActivity : AppCompatActivity() {
(synchronizer as SdkSynchronizer).processor.onScanMetricCompleteListener = ::onScanMetricComplete
synchronizer.start(lifecycleScope)
mainViewModel.setSyncReady(true)
}
} else {
twig("Ignoring request to start sync because sync has already been started!")
@ -311,24 +309,6 @@ class MainActivity : AppCompatActivity() {
mainViewModel.setLoading(isLoading, message)
}
/**
* Launch the given block if the synchronizer is ready and syncing. Otherwise, wait until it is.
* The block will be scoped to the synchronizer when it runs.
*/
fun launchWhenSyncing(block: suspend CoroutineScope.() -> Unit) {
// TODO: update this quick and dirty implementation, after the holidays. For now, this gets
// the job done but the synchronizer should expose a method that helps with this so that
// any complexity is taken care of at the library level.
lifecycleScope.launch {
while (mainViewModel.isLoading) {
delay(25L)
}
(synchronizerComponent.synchronizer() as SdkSynchronizer).coroutineScope.launch {
block()
}
}
}
fun authenticate(description: String, title: String = getString(R.string.biometric_prompt_title), block: () -> Unit) {
val callback = object : BiometricPrompt.AuthenticationCallback() {
override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {

View File

@ -4,13 +4,23 @@ import androidx.lifecycle.ViewModel
import cash.z.ecc.android.sdk.ext.twig
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import javax.inject.Inject
class MainViewModel @Inject constructor() : ViewModel() {
private val _loadingMessage = MutableStateFlow<String?>("\u23F3 Loading...")
private val _syncReady = MutableStateFlow(false)
val loadingMessage: StateFlow<String?> get() = _loadingMessage
val isLoading get() = loadingMessage.value != null
/**
* A flow of booleans representing whether or not the synchronizer has been started. This is
* useful for views that want to monitor the status of the wallet but don't want to access the
* synchronizer before it is ready to be used. This is also helpful for race conditions where
* the status of the synchronizer is needed before it is created.
*/
val syncReady = _syncReady.asStateFlow()
fun setLoading(isLoading: Boolean = false, message: String? = null) {
twig("MainViewModel.setLoading: $isLoading")
_loadingMessage.value = if (!isLoading) {
@ -19,4 +29,9 @@ class MainViewModel @Inject constructor() : ViewModel() {
message ?: "\u23F3 Loading..."
}
}
fun setSyncReady(isReady: Boolean) {
twig("MainViewModel.setSyncReady: $isReady")
_syncReady.value = isReady
}
}

View File

@ -11,6 +11,9 @@ import androidx.viewbinding.ViewBinding
import cash.z.ecc.android.feedback.Report
import cash.z.ecc.android.ui.MainActivity
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.onEach
abstract class BaseFragment<T : ViewBinding> : Fragment() {
val mainActivity: MainActivity? get() = activity as MainActivity?
@ -58,4 +61,22 @@ abstract class BaseFragment<T : ViewBinding> : Fragment() {
mainActivity?.reportTap(tap)
}
/**
* Launch the given block once, within the 'resumedScope', once the Synchronizer is ready. This
* utility function helps solve the problem of taking action with the synchronizer before it
* is created. This surfaced while loading keys from secure storage: the HomeFragment would
* resume and start monitoring the synchronizer for changes BEFORE the onAttach function
* returned, meaning before the synchronizerComponent is created. So a state variable needed to
* exist with a longer lifecycle than the synchronizer. This function just takes care of all the
* boilerplate of monitoring that state variable until it returns true.
*/
fun launchWhenSyncReady(block: () -> Unit) {
resumedScope.launch {
mainActivity?.let {
it.mainViewModel.syncReady.filter { isReady -> isReady }.onEach {
block()
}.first()
}
}
}
}

View File

@ -76,13 +76,11 @@ class TransactionViewHolder<T : ConfirmedTransaction>(itemView: View) : Recycler
arrowRotation = R.integer.transaction_arrow_rotation_send
amountColor = R.color.transaction_sent
if (toAddress.isShielded()) {
arrowBackgroundTint = R.color.zcashYellow
lineOneColor = R.color.zcashYellow
} else {
toAddress?.toAbbreviatedAddress()?.let {
lineOne = lineOne.toColoredSpan(R.color.zcashBlueDark, it)
}
isLineOneSpanned = true
}
} else {
arrowRotation = R.integer.transaction_arrow_rotation_pending
@ -97,7 +95,6 @@ class TransactionViewHolder<T : ConfirmedTransaction>(itemView: View) : Recycler
if (senderAddress.isShielded()) {
amountColor = R.color.zcashYellow
lineOneColor = R.color.zcashYellow
arrowBackgroundTint = R.color.zcashYellow
} else {
senderAddress.toAbbreviatedAddress().let {
lineOne = if (senderAddress.equals(str(R.string.unknown), true)) {
@ -106,7 +103,6 @@ class TransactionViewHolder<T : ConfirmedTransaction>(itemView: View) : Recycler
lineOne.toColoredSpan(R.color.zcashBlueDark, it)
}
}
isLineOneSpanned = true
}
arrowRotation = R.integer.transaction_arrow_rotation_received
}

View File

@ -9,6 +9,7 @@ import android.widget.TextView
import android.widget.Toast
import androidx.appcompat.content.res.AppCompatResources
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.LinearLayoutManager
import cash.z.ecc.android.R
import cash.z.ecc.android.databinding.DialogSolicitFeedbackRatingBinding
import cash.z.ecc.android.databinding.FragmentHomeBinding
@ -162,31 +163,17 @@ class HomeFragment : BaseFragment<FragmentHomeBinding>() {
override fun onResume() {
super.onResume()
maybeInterruptUser()
mainActivity?.launchWhenSyncing {
twig("HomeFragment.onResume resumeScope.isActive: ${resumedScope.isActive} $resumedScope")
val existingAmount = sendViewModel.zatoshiAmount.coerceAtLeast(0)
viewModel.initializeMaybe(WalletZecFormmatter.toZecStringFull(existingAmount))
if (existingAmount == 0L) onClearAmount()
viewModel.uiModels.runningReduce { old, new ->
onModelUpdated(old, new)
new
}.onCompletion {
twig("uiModel.scanReduce completed.")
}.catch { e ->
twig("exception while processing uiModels $e")
throw e
}.launchIn(resumedScope)
twig("HomeFragment.onResume resumeScope.isActive: ${resumedScope.isActive} $resumedScope")
// TODO: see if there is a better way to trigger a refresh of the uiModel on resume
// the latest one should just be in the viewmodel and we should just "resubscribe"
// but for some reason, this doesn't always happen, which kind of defeats the purpose
// of having a cold stream in the view model
resumedScope.launch {
viewModel.refreshBalance()
}
twig("HomeFragment.onResume COMPLETE")
}
launchWhenSyncReady(::onSyncReady)
}
private fun onSyncReady() {
twig("Sync ready! Monitoring synchronizer state...")
monitorUiModelChanges()
maybeInterruptUser()
twig("HomeFragment.onSyncReady COMPLETE")
}
override fun onSaveInstanceState(outState: Bundle) {
@ -411,6 +398,21 @@ class HomeFragment : BaseFragment<FragmentHomeBinding>() {
setBanner(getString(R.string.home_no_balance), FUND_NOW)
}
private fun monitorUiModelChanges() {
val existingAmount = sendViewModel.zatoshiAmount.coerceAtLeast(0)
viewModel.initializeMaybe(WalletZecFormmatter.toZecStringFull(existingAmount))
if (existingAmount == 0L) onClearAmount()
viewModel.uiModels.runningReduce { old, new ->
onModelUpdated(old, new)
new
}.onCompletion {
twig("uiModel.scanReduce completed.")
}.catch { e ->
twig("exception while processing uiModels $e")
throw e
}.launchIn(resumedScope)
}
//
// Inner classes and extensions

View File

@ -47,7 +47,7 @@ class SendFinalFragment : BaseFragment<FragmentSendFinalBinding>() {
mainActivity?.apply {
sendViewModel.send().onEach {
onPendingTxUpdated(it)
}.launchIn(lifecycleScope)
}.launchIn(resumedScope)
}
}

View File

@ -95,14 +95,18 @@ class WalletSetupViewModel @Inject constructor() : ViewModel() {
*/
private suspend fun loadConfig(): Initializer.Config {
twig("Loading config variables")
var overwriteVks = false
val network = ZcashWalletApp.instance.defaultNetwork
val vk = loadUnifiedViewingKey() ?: onMissingViewingKey(network)
val vk = loadUnifiedViewingKey() ?: onMissingViewingKey(network).also { overwriteVks = true }
val birthdayHeight = loadBirthdayHeight() ?: onMissingBirthday(network)
val host = prefs[Const.Pref.SERVER_HOST] ?: Const.Default.Server.HOST
val port = prefs[Const.Pref.SERVER_PORT] ?: Const.Default.Server.PORT
twig("Done loading config variables")
return Initializer.Config { it.importWallet(vk, birthdayHeight, network, host, port) }
return Initializer.Config {
it.importWallet(vk, birthdayHeight, network, host, port)
it.setOverwriteKeys(overwriteVks)
}
}
private fun loadUnifiedViewingKey(): UnifiedViewingKey? {

View File

@ -30,7 +30,7 @@
android:text="1"
android:textColor="@color/text_dark"
android:textSize="30dp"
app:layout_constraintBottom_toBottomOf="@id/background_buttons"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@id/feedback_exp_2"
app:layout_constraintHorizontal_chainStyle="spread_inside"
app:layout_constraintStart_toStartOf="parent"
@ -46,7 +46,7 @@
android:text="2"
android:textColor="@color/text_dark"
android:textSize="30dp"
app:layout_constraintBottom_toBottomOf="@id/background_buttons"
app:layout_constraintBottom_toBottomOf="@id/feedback_exp_1"
app:layout_constraintEnd_toStartOf="@id/feedback_exp_3"
app:layout_constraintStart_toEndOf="@id/feedback_exp_1"
app:layout_constraintTop_toTopOf="@id/feedback_exp_1" />
@ -61,7 +61,7 @@
android:text="3"
android:textColor="@color/text_dark"
android:textSize="30dp"
app:layout_constraintBottom_toBottomOf="@id/background_buttons"
app:layout_constraintBottom_toBottomOf="@id/feedback_exp_1"
app:layout_constraintEnd_toStartOf="@id/feedback_exp_4"
app:layout_constraintStart_toEndOf="@id/feedback_exp_2"
app:layout_constraintTop_toTopOf="@id/feedback_exp_1" />
@ -76,7 +76,7 @@
android:text="4"
android:textColor="@color/text_dark"
android:textSize="30dp"
app:layout_constraintBottom_toBottomOf="@id/background_buttons"
app:layout_constraintBottom_toBottomOf="@id/feedback_exp_1"
app:layout_constraintEnd_toStartOf="@id/feedback_exp_5"
app:layout_constraintStart_toEndOf="@id/feedback_exp_3"
app:layout_constraintTop_toTopOf="@id/feedback_exp_1" />

View File

@ -21,6 +21,7 @@ allprojects {
repositories {
// mavenLocal()
google()
mavenCentral()
jcenter()
maven { url 'https://jitpack.io' }
}

View File

@ -10,8 +10,8 @@ object Deps {
const val buildToolsVersion = "29.0.2"
const val minSdkVersion = 21
const val targetSdkVersion = 29
const val versionName = "1.0.0-alpha67"
const val versionCode = 1_00_00_167 // last digits are alpha(0XX) beta(2XX) rc(4XX) release(8XX). Ex: 1_08_04_401 is an release candidate build of version 1.8.4 and 1_08_04_800 would be the final release.
const val versionName = "1.0.0-alpha69"
const val versionCode = 1_00_00_169 // last digits are alpha(0XX) beta(2XX) rc(4XX) release(8XX). Ex: 1_08_04_401 is an release candidate build of version 1.8.4 and 1_08_04_800 would be the final release.
const val packageName = "cash.z.ecc.android"
@ -83,7 +83,7 @@ object Deps {
object Zcash {
const val ANDROID_WALLET_PLUGINS = "cash.z.ecc.android:zcash-android-wallet-plugins:1.0.0"
const val KOTLIN_BIP39 = "cash.z.ecc.android:kotlin-bip39:1.0.1"
const val SDK = "cash.z.ecc.android:zcash-android-sdk:1.3.0-beta05"
const val SDK = "cash.z.ecc.android:zcash-android-sdk:1.3.0-beta08"
}
object Misc {
const val LOTTIE = "com.airbnb.android:lottie:3.1.0"