Dagger removal (#347)

* dagger removal

* analytics removal
This commit is contained in:
Alex 2022-10-31 13:04:14 +01:00 committed by GitHub
parent f397260430
commit d14637012c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
59 changed files with 686 additions and 1136 deletions

View File

@ -160,11 +160,6 @@ dependencies {
implementation Deps.Google.GUAVA
implementation Deps.Google.MATERIAL
// Dagger
implementation Deps.Dagger.ANDROID_SUPPORT
kapt Deps.Dagger.ANDROID_PROCESSOR
kapt Deps.Dagger.COMPILER
// grpc-java
implementation Deps.Grpc.ANDROID
implementation Deps.Grpc.OKHTTP
@ -173,10 +168,6 @@ dependencies {
implementation 'com.squareup.okio:okio:2.8.0'
implementation Deps.JavaX.JAVA_ANNOTATION
// Analytics (for dogfooding/crash-reporting/feedback only on internal team builds)
implementation Deps.Analytics.MIXPANEL
implementation Deps.Analytics.BUGSNAG
// Misc.
implementation Deps.Misc.LOTTIE
implementation Deps.Misc.CHIPS

View File

@ -10,7 +10,7 @@ import cash.z.ecc.kotlin.mnemonic.Mnemonics
import kotlinx.coroutines.test.runTest
import okio.Buffer
import okio.GzipSink
import okio.Okio
import okio.buffer
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Before
@ -106,7 +106,7 @@ class IntegrationTest {
fun String.gzip(): ByteArray {
val result = Buffer()
val sink = Okio.buffer(GzipSink(result))
val sink = GzipSink(result).buffer()
sink.use {
sink.write(toByteArray())
}

View File

@ -4,26 +4,18 @@ import android.app.Application
import android.content.Context
import androidx.camera.camera2.Camera2Config
import androidx.camera.core.CameraXConfig
import cash.z.ecc.android.di.component.AppComponent
import cash.z.ecc.android.di.component.DaggerAppComponent
import cash.z.ecc.android.di.DependenciesHolder
import cash.z.ecc.android.ext.tryWithWarning
import cash.z.ecc.android.feedback.FeedbackCoordinator
import cash.z.ecc.android.sdk.ext.ZcashSdk
import cash.z.ecc.android.sdk.internal.Twig
import cash.z.ecc.android.sdk.model.Zatoshi
import cash.z.ecc.android.sdk.model.ZcashNetwork
import cash.z.ecc.android.util.twig
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import javax.inject.Inject
import kotlinx.coroutines.*
class ZcashWalletApp : Application(), CameraXConfig.Provider {
@Inject
lateinit var coordinator: FeedbackCoordinator
private val coordinator: FeedbackCoordinator
get() = DependenciesHolder.feedbackCoordinator
lateinit var defaultNetwork: ZcashNetwork
@ -74,8 +66,6 @@ class ZcashWalletApp : Application(), CameraXConfig.Provider {
creationTime = System.currentTimeMillis()
defaultNetwork = ZcashNetwork.from(resources.getInteger(R.integer.zcash_network_id))
component = DaggerAppComponent.factory().create(this)
component.inject(this)
feedbackScope.launch {
coordinator.feedback.start()
}
@ -87,7 +77,6 @@ class ZcashWalletApp : Application(), CameraXConfig.Provider {
companion object {
lateinit var instance: ZcashWalletApp
lateinit var component: AppComponent
}
/**

View File

@ -0,0 +1,44 @@
package cash.z.ecc.android.di
import android.content.ClipboardManager
import android.content.Context
import cash.z.ecc.android.ZcashWalletApp
import cash.z.ecc.android.ext.Const
import cash.z.ecc.android.feedback.*
import cash.z.ecc.android.lockbox.LockBox
import cash.z.ecc.android.sdk.Synchronizer
import cash.z.ecc.android.ui.util.DebugFileTwig
import cash.z.ecc.android.util.SilentTwig
import cash.z.ecc.android.util.Twig
import cash.z.ecc.kotlin.mnemonic.Mnemonics
object DependenciesHolder {
fun provideAppContext(): Context = ZcashWalletApp.instance
val initializerComponent by lazy { InitializerComponent() }
val synchronizer by lazy { Synchronizer.newBlocking(initializerComponent.initializer) }
val clipboardManager by lazy { provideAppContext().getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager }
val lockBox by lazy { LockBox(provideAppContext()) }
val prefs by lazy { LockBox(provideAppContext()) }
val feedback by lazy { Feedback() }
val feedbackCoordinator by lazy {
lockBox.getBoolean(Const.Pref.FEEDBACK_ENABLED).let { isEnabled ->
// observe nothing unless feedback is enabled
Twig.plant(if (isEnabled) DebugFileTwig() else SilentTwig())
FeedbackCoordinator(feedback)
}
}
val feedbackFile by lazy { FeedbackFile() }
val feedbackConsole by lazy { FeedbackConsole() }
val mnemonics by lazy { Mnemonics() }
}

View File

@ -0,0 +1,14 @@
package cash.z.ecc.android.di
import cash.z.ecc.android.sdk.Initializer
class InitializerComponent {
lateinit var initializer: Initializer
private set
fun createInitializer(config: Initializer.Config) {
initializer = Initializer.newBlocking(DependenciesHolder.provideAppContext(), config)
}
}

View File

@ -1,7 +0,0 @@
package cash.z.ecc.android.di.annotation
import javax.inject.Scope
@Scope
@Retention(AnnotationRetention.SOURCE)
annotation class ActivityScope

View File

@ -1,7 +0,0 @@
package cash.z.ecc.android.di.annotation
import javax.inject.Scope
@Scope
@Retention(AnnotationRetention.SOURCE)
annotation class ApplicationScope

View File

@ -1,7 +0,0 @@
package cash.z.ecc.android.di.annotation
import javax.inject.Scope
@Scope
@Retention(AnnotationRetention.SOURCE)
annotation class FragmentScope

View File

@ -1,7 +0,0 @@
package cash.z.ecc.android.di.annotation
import javax.inject.Scope
@Scope
@Retention(AnnotationRetention.SOURCE)
annotation class SynchronizerScope

View File

@ -1,14 +0,0 @@
package cash.z.ecc.android.di.annotation
import androidx.lifecycle.ViewModel
import dagger.MapKey
import kotlin.reflect.KClass
@Target(
AnnotationTarget.FUNCTION,
AnnotationTarget.PROPERTY_GETTER,
AnnotationTarget.PROPERTY_SETTER
)
@Retention(AnnotationRetention.RUNTIME)
@MapKey
annotation class ViewModelKey(val value: KClass<out ViewModel>)

View File

@ -1,23 +0,0 @@
package cash.z.ecc.android.di.component
import cash.z.ecc.android.ZcashWalletApp
import cash.z.ecc.android.di.module.AppModule
import dagger.BindsInstance
import dagger.Component
import javax.inject.Singleton
@Singleton
@Component(modules = [AppModule::class])
interface AppComponent {
fun inject(zcashWalletApp: ZcashWalletApp)
// Subcomponents
fun mainActivitySubcomponent(): MainActivitySubcomponent.Factory
fun synchronizerSubcomponent(): SynchronizerSubcomponent.Factory
fun initializerSubcomponent(): InitializerSubcomponent.Factory
@Component.Factory
interface Factory {
fun create(@BindsInstance application: ZcashWalletApp): AppComponent
}
}

View File

@ -1,20 +0,0 @@
package cash.z.ecc.android.di.component
import cash.z.ecc.android.di.annotation.SynchronizerScope
import cash.z.ecc.android.di.module.InitializerModule
import cash.z.ecc.android.sdk.Initializer
import dagger.BindsInstance
import dagger.Subcomponent
@SynchronizerScope
@Subcomponent(modules = [InitializerModule::class])
interface InitializerSubcomponent {
fun initializer(): Initializer
fun config(): Initializer.Config
@Subcomponent.Factory
interface Factory {
fun create(@BindsInstance config: Initializer.Config): InitializerSubcomponent
}
}

View File

@ -1,25 +0,0 @@
package cash.z.ecc.android.di.component
import androidx.fragment.app.FragmentActivity
import androidx.lifecycle.ViewModelProvider
import cash.z.ecc.android.di.annotation.ActivityScope
import cash.z.ecc.android.di.module.MainActivityModule
import cash.z.ecc.android.ext.Const
import cash.z.ecc.android.ui.MainActivity
import dagger.BindsInstance
import dagger.Subcomponent
import javax.inject.Named
@ActivityScope
@Subcomponent(modules = [MainActivityModule::class])
interface MainActivitySubcomponent {
fun inject(activity: MainActivity)
@Named(Const.Name.BEFORE_SYNCHRONIZER) fun viewModelFactory(): ViewModelProvider.Factory
@Subcomponent.Factory
interface Factory {
fun create(@BindsInstance activity: FragmentActivity): MainActivitySubcomponent
}
}

View File

@ -1,25 +0,0 @@
package cash.z.ecc.android.di.component
import androidx.lifecycle.ViewModelProvider
import cash.z.ecc.android.di.annotation.SynchronizerScope
import cash.z.ecc.android.di.module.SynchronizerModule
import cash.z.ecc.android.ext.Const
import cash.z.ecc.android.sdk.Initializer
import cash.z.ecc.android.sdk.Synchronizer
import dagger.BindsInstance
import dagger.Subcomponent
import javax.inject.Named
@SynchronizerScope
@Subcomponent(modules = [SynchronizerModule::class])
interface SynchronizerSubcomponent {
fun synchronizer(): Synchronizer
@Named(Const.Name.SYNCHRONIZER) fun viewModelFactory(): ViewModelProvider.Factory
@Subcomponent.Factory
interface Factory {
fun create(@BindsInstance initializer: Initializer): SynchronizerSubcomponent
}
}

View File

@ -1,87 +0,0 @@
package cash.z.ecc.android.di.module
import android.content.ClipboardManager
import android.content.Context
import cash.z.ecc.android.ZcashWalletApp
import cash.z.ecc.android.di.component.MainActivitySubcomponent
import cash.z.ecc.android.ext.Const
import cash.z.ecc.android.feedback.Feedback
import cash.z.ecc.android.feedback.FeedbackBugsnag
import cash.z.ecc.android.feedback.FeedbackConsole
import cash.z.ecc.android.feedback.FeedbackCoordinator
import cash.z.ecc.android.feedback.FeedbackFile
import cash.z.ecc.android.feedback.FeedbackMixpanel
import cash.z.ecc.android.lockbox.LockBox
import cash.z.ecc.android.ui.util.DebugFileTwig
import cash.z.ecc.android.util.SilentTwig
import cash.z.ecc.android.util.Twig
import dagger.Module
import dagger.Provides
import dagger.multibindings.IntoSet
import javax.inject.Named
import javax.inject.Singleton
@Module(subcomponents = [MainActivitySubcomponent::class])
class AppModule {
@Provides
@Singleton
fun provideAppContext(): Context = ZcashWalletApp.instance
@Provides
@Singleton
fun provideClipboard(context: Context) =
context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
@Provides
@Named(Const.Name.APP_PREFS)
fun provideLockbox(appContext: Context): LockBox {
return LockBox(appContext)
}
//
// Feedback
//
@Provides
@Singleton
fun provideFeedback(): Feedback = Feedback()
@Provides
@Singleton
fun provideFeedbackCoordinator(
feedback: Feedback,
@Named(Const.Name.APP_PREFS) prefs: LockBox,
defaultObservers: Set<@JvmSuppressWildcards FeedbackCoordinator.FeedbackObserver>
): FeedbackCoordinator {
return prefs.getBoolean(Const.Pref.FEEDBACK_ENABLED).let { isEnabled ->
// observe nothing unless feedback is enabled
Twig.plant(if (isEnabled) DebugFileTwig() else SilentTwig())
FeedbackCoordinator(feedback, if (isEnabled) defaultObservers else setOf())
}
}
//
// Default Feedback Observer Set
//
@Provides
@Singleton
@IntoSet
fun provideFeedbackFile(): FeedbackCoordinator.FeedbackObserver = FeedbackFile()
@Provides
@Singleton
@IntoSet
fun provideFeedbackConsole(): FeedbackCoordinator.FeedbackObserver = FeedbackConsole()
@Provides
@Singleton
@IntoSet
fun provideFeedbackMixpanel(): FeedbackCoordinator.FeedbackObserver = FeedbackMixpanel()
@Provides
@Singleton
@IntoSet
fun provideFeedbackBugsnag(): FeedbackCoordinator.FeedbackObserver = FeedbackBugsnag()
}

View File

@ -1,15 +0,0 @@
package cash.z.ecc.android.di.module
import android.content.Context
import cash.z.ecc.android.sdk.Initializer
import dagger.Module
import dagger.Provides
import dagger.Reusable
@Module
class InitializerModule {
@Provides
@Reusable
fun provideInitializer(appContext: Context, config: Initializer.Config) = Initializer.newBlocking(appContext, config)
}

View File

@ -1,11 +0,0 @@
package cash.z.ecc.android.di.module
import cash.z.ecc.android.di.component.InitializerSubcomponent
import cash.z.ecc.android.di.component.SynchronizerSubcomponent
import dagger.Module
@Module(
includes = [ViewModelsActivityModule::class],
subcomponents = [SynchronizerSubcomponent::class, InitializerSubcomponent::class]
)
class MainActivityModule

View File

@ -1,23 +0,0 @@
package cash.z.ecc.android.di.module
import cash.z.ecc.android.di.annotation.SynchronizerScope
import cash.z.ecc.android.sdk.Initializer
import cash.z.ecc.android.sdk.Synchronizer
import dagger.Module
import dagger.Provides
/**
* Module that creates the synchronizer from an initializer and also everything that depends on the
* synchronizer (because it doesn't exist prior to this module being installed).
*/
@Module(includes = [ViewModelsSynchronizerModule::class])
class SynchronizerModule {
private var synchronizer: Synchronizer? = null
@Provides
@SynchronizerScope
fun provideSynchronizer(initializer: Initializer): Synchronizer {
return synchronizer ?: Synchronizer.newBlocking(initializer).also { synchronizer = it }
}
}

View File

@ -1,39 +0,0 @@
package cash.z.ecc.android.di.module
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import cash.z.ecc.android.di.annotation.ActivityScope
import cash.z.ecc.android.di.annotation.ViewModelKey
import cash.z.ecc.android.di.viewmodel.ViewModelFactory
import cash.z.ecc.android.ext.Const
import cash.z.ecc.android.ui.setup.WalletSetupViewModel
import dagger.Binds
import dagger.Module
import dagger.multibindings.IntoMap
import javax.inject.Named
/**
* View model related objects, scoped to the activity that do not depend on the Synchronizer. These
* are any VMs that must be created before the Synchronizer.
*/
@Module
abstract class ViewModelsActivityModule {
@ActivityScope
@Binds
@IntoMap
@ViewModelKey(WalletSetupViewModel::class)
abstract fun bindWalletSetupViewModel(implementation: WalletSetupViewModel): ViewModel
/**
* Factory for view models that are created until before the Synchronizer exists. This is a
* little tricky because we cannot make them all in one place or else they won't be available
* to both the parent and the child components. If they all live in the child component, which
* isn't created until the synchronizer exists, then the parent component will not have the
* view models yet.
*/
@ActivityScope
@Named(Const.Name.BEFORE_SYNCHRONIZER)
@Binds
abstract fun bindViewModelFactory(viewModelFactory: ViewModelFactory): ViewModelProvider.Factory
}

View File

@ -1,91 +0,0 @@
package cash.z.ecc.android.di.module
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import cash.z.ecc.android.di.annotation.SynchronizerScope
import cash.z.ecc.android.di.annotation.ViewModelKey
import cash.z.ecc.android.di.viewmodel.ViewModelFactory
import cash.z.ecc.android.ext.Const
import cash.z.ecc.android.ui.history.HistoryViewModel
import cash.z.ecc.android.ui.home.BalanceDetailViewModel
import cash.z.ecc.android.ui.home.HomeViewModel
import cash.z.ecc.android.ui.profile.ProfileViewModel
import cash.z.ecc.android.ui.receive.ReceiveViewModel
import cash.z.ecc.android.ui.scan.ScanViewModel
import cash.z.ecc.android.ui.send.AutoShieldViewModel
import cash.z.ecc.android.ui.send.SendViewModel
import cash.z.ecc.android.ui.settings.SettingsViewModel
import dagger.Binds
import dagger.Module
import dagger.multibindings.IntoMap
import javax.inject.Named
/**
* View model related objects, scoped to the synchronizer.
*/
@Module
abstract class ViewModelsSynchronizerModule {
@SynchronizerScope
@Binds
@IntoMap
@ViewModelKey(HomeViewModel::class)
abstract fun bindHomeViewModel(implementation: HomeViewModel): ViewModel
@SynchronizerScope
@Binds
@IntoMap
@ViewModelKey(SendViewModel::class)
abstract fun bindSendViewModel(implementation: SendViewModel): ViewModel
@SynchronizerScope
@Binds
@IntoMap
@ViewModelKey(HistoryViewModel::class)
abstract fun bindHistoryViewModel(implementation: HistoryViewModel): ViewModel
@SynchronizerScope
@Binds
@IntoMap
@ViewModelKey(ReceiveViewModel::class)
abstract fun bindReceiveViewModel(implementation: ReceiveViewModel): ViewModel
@SynchronizerScope
@Binds
@IntoMap
@ViewModelKey(ScanViewModel::class)
abstract fun bindScanViewModel(implementation: ScanViewModel): ViewModel
@SynchronizerScope
@Binds
@IntoMap
@ViewModelKey(ProfileViewModel::class)
abstract fun bindProfileViewModel(implementation: ProfileViewModel): ViewModel
@SynchronizerScope
@Binds
@IntoMap
@ViewModelKey(SettingsViewModel::class)
abstract fun bindSettingsViewModel(implementation: SettingsViewModel): ViewModel
@SynchronizerScope
@Binds
@IntoMap
@ViewModelKey(BalanceDetailViewModel::class)
abstract fun bindBalanceDetailViewModel(implementation: BalanceDetailViewModel): ViewModel
@SynchronizerScope
@Binds
@IntoMap
@ViewModelKey(AutoShieldViewModel::class)
abstract fun bindAutoShieldViewModel(implementation: AutoShieldViewModel): ViewModel
/**
* Factory for view models that are not created until the Synchronizer exists. Only VMs that
* require the Synchronizer should wait until it is created. In other words, these are the VMs
* that live within the scope of the Synchronizer.
*/
@SynchronizerScope
@Named(Const.Name.SYNCHRONIZER)
@Binds
abstract fun bindViewModelFactory(viewModelFactory: ViewModelFactory): ViewModelProvider.Factory
}

View File

@ -1,60 +0,0 @@
package cash.z.ecc.android.di.viewmodel
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import cash.z.ecc.android.ui.MainActivity
import cash.z.ecc.android.ui.base.BaseFragment
inline fun <reified VM : ViewModel> BaseFragment<*>.viewModel() = object : Lazy<VM> {
val cached: VM? = null
override fun isInitialized(): Boolean = cached != null
override val value: VM
get() = cached
?: ViewModelProvider(this@viewModel, scopedFactory<VM>())[VM::class.java]
}
/**
* Create a view model that is scoped to the lifecycle of the activity.
*
* @param isSynchronizerScope true when this view model depends on the Synchronizer. False when this
* viewModel needs to be created before the synchronizer or otherwise has no dependency on it being
* available for use.
*/
inline fun <reified VM : ViewModel> BaseFragment<*>.activityViewModel(isSynchronizerScope: Boolean = true) = object : Lazy<VM> {
val cached: VM? = null
override fun isInitialized(): Boolean = cached != null
override val value: VM
get() {
return cached
?: scopedFactory<VM>(isSynchronizerScope)?.let { factory ->
ViewModelProvider(this@activityViewModel.mainActivity!!, factory)[VM::class.java]
}!!
}
}
inline fun <reified VM : ViewModel> BaseFragment<*>.scopedFactory(isSynchronizerScope: Boolean = true): ViewModelProvider.Factory {
val factory = if (isSynchronizerScope) mainActivity?.synchronizerComponent?.viewModelFactory() else mainActivity?.component?.viewModelFactory()
return factory ?: throw IllegalStateException("Error: mainActivity should not be null by the time the ${VM::class.java.simpleName} viewmodel is lazily accessed!")
}
/**
* Create a viewModel that is scoped to the lifecycle of the activity. This viewModel will be
* created from the `synchronizerComponent` rather than the `component`, meaning the synchronizer
* will be available but this also requires that this view model not be accessed before the
* synchronizerComponent is ready. Doing so will throw an exception.
*/
inline fun <reified VM : ViewModel> MainActivity.activityViewModel() = object : Lazy<VM> {
val cached: VM? = null
override fun isInitialized(): Boolean = cached != null
override val value: VM
get() {
return cached
?: this@activityViewModel.run {
if (isInitialized) {
ViewModelProvider(this, synchronizerComponent.viewModelFactory())[VM::class.java]
} else {
throw IllegalStateException("Error: the SynchronizerComponent must be initialized before the ${VM::class.java.simpleName} viewmodel is lazily accessed!")
}
}
}
}

View File

@ -1,21 +0,0 @@
package cash.z.ecc.android.di.viewmodel
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import javax.inject.Inject
import javax.inject.Provider
class ViewModelFactory @Inject constructor(
private val creators: Map<Class<out ViewModel>, @JvmSuppressWildcards Provider<ViewModel>>
) : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
val creator = creators[modelClass] ?: creators.entries.firstOrNull {
modelClass.isAssignableFrom(it.key)
}?.value ?: throw IllegalArgumentException(
"No map entry found for ${modelClass.canonicalName}. Verify that this ViewModel has" +
" been added to the ViewModelModule. ${creators.keys}"
)
@Suppress("UNCHECKED_CAST")
return creator.get() as T
}
}

View File

@ -1,64 +0,0 @@
package cash.z.ecc.android.feedback
import cash.z.ecc.android.R
import cash.z.ecc.android.ZcashWalletApp
import cash.z.ecc.android.util.twig
import com.bugsnag.android.Bugsnag
import com.bugsnag.android.Configuration
class FeedbackBugsnag : FeedbackCoordinator.FeedbackObserver {
var isInitialized = false
override fun initialize(): FeedbackCoordinator.FeedbackObserver = apply {
ZcashWalletApp.instance.let { appContext ->
appContext.getString(R.string.bugsnag_api_key)
.takeUnless { it.isNullOrEmpty() }?.let { apiKey ->
twig("starting bugsnag")
val config = Configuration(apiKey)
Bugsnag.start(appContext, config)
isInitialized = true
} ?: onInitError()
}
}
private fun onInitError() {
twig("Warning: Failed to load bugsnag because the API key was missing!")
}
/**
* Report non-fatal crashes because fatal ones already get reported by default.
*/
override fun onAction(action: Feedback.Action) {
if (!isInitialized) return
when (action) {
is Feedback.Crash -> action.exception
is Feedback.NonFatal -> action.exception
is Report.Error.NonFatal.Reorg -> ReorgException(
action.errorHeight,
action.rewindHeight,
action.toString()
)
is Report.Funnel.Send.Error -> SendException(
action.errorCode,
action.errorMessage
)
else -> null
}?.let { exception ->
// fix: always add details so that we can differentiate a lack of details from a change in the way details should be added
val details = kotlin.runCatching { action.toMap() }.getOrElse { mapOf("hasDetails" to false) }
Bugsnag.notify(exception) { event ->
event.addMetadata("errorDetails", details)
true
}
}
}
private class ReorgException(errorHeight: Int, rewindHeight: Int, reorgMesssage: String) :
Throwable(reorgMesssage)
private class SendException(errorCode: Int?, errorMessage: String?) : RuntimeException(
"Non-fatal error while sending transaction. code: $errorCode message: $errorMessage"
)
}

View File

@ -1,32 +0,0 @@
package cash.z.ecc.android.feedback
import cash.z.ecc.android.R
import cash.z.ecc.android.ZcashWalletApp
import cash.z.ecc.android.ext.toAppString
import com.mixpanel.android.mpmetrics.MixpanelAPI
class FeedbackMixpanel : FeedbackCoordinator.FeedbackObserver {
private lateinit var mixpanel: MixpanelAPI
override fun initialize(): FeedbackCoordinator.FeedbackObserver = apply {
mixpanel =
MixpanelAPI.getInstance(ZcashWalletApp.instance, R.string.mixpanel_project.toAppString())
}
override fun onMetric(metric: Feedback.Metric) {
track(metric.key, metric.toMap())
}
override fun onAction(action: Feedback.Action) {
track(action.key, action.toMap())
}
override fun flush() {
mixpanel.flush()
}
private fun track(eventName: String, properties: Map<String, Any>) {
mixpanel.trackMap(eventName, properties)
}
}

View File

@ -21,28 +21,13 @@ import android.view.inputmethod.InputMethodManager
import android.widget.TextView
import android.widget.Toast
import androidx.activity.OnBackPressedCallback
import androidx.activity.viewModels
import androidx.annotation.IdRes
import androidx.annotation.StringRes
import androidx.appcompat.app.AppCompatActivity
import androidx.biometric.BiometricManager.Authenticators.BIOMETRIC_STRONG
import androidx.biometric.BiometricManager.Authenticators.BIOMETRIC_WEAK
import androidx.biometric.BiometricManager.Authenticators.DEVICE_CREDENTIAL
import androidx.biometric.BiometricManager.Authenticators.*
import androidx.biometric.BiometricPrompt
import androidx.biometric.BiometricPrompt.AUTHENTICATION_RESULT_TYPE_BIOMETRIC
import androidx.biometric.BiometricPrompt.AUTHENTICATION_RESULT_TYPE_DEVICE_CREDENTIAL
import androidx.biometric.BiometricPrompt.ERROR_CANCELED
import androidx.biometric.BiometricPrompt.ERROR_HW_NOT_PRESENT
import androidx.biometric.BiometricPrompt.ERROR_HW_UNAVAILABLE
import androidx.biometric.BiometricPrompt.ERROR_LOCKOUT
import androidx.biometric.BiometricPrompt.ERROR_LOCKOUT_PERMANENT
import androidx.biometric.BiometricPrompt.ERROR_NEGATIVE_BUTTON
import androidx.biometric.BiometricPrompt.ERROR_NO_BIOMETRICS
import androidx.biometric.BiometricPrompt.ERROR_NO_DEVICE_CREDENTIAL
import androidx.biometric.BiometricPrompt.ERROR_NO_SPACE
import androidx.biometric.BiometricPrompt.ERROR_TIMEOUT
import androidx.biometric.BiometricPrompt.ERROR_UNABLE_TO_PROCESS
import androidx.biometric.BiometricPrompt.ERROR_USER_CANCELED
import androidx.biometric.BiometricPrompt.ERROR_VENDOR
import androidx.biometric.BiometricPrompt.*
import androidx.core.content.ContextCompat
import androidx.core.content.getSystemService
import androidx.fragment.app.Fragment
@ -54,14 +39,8 @@ import androidx.navigation.findNavController
import cash.z.ecc.android.R
import cash.z.ecc.android.ZcashWalletApp
import cash.z.ecc.android.databinding.DialogFirstUseMessageBinding
import cash.z.ecc.android.di.component.MainActivitySubcomponent
import cash.z.ecc.android.di.component.SynchronizerSubcomponent
import cash.z.ecc.android.di.viewmodel.activityViewModel
import cash.z.ecc.android.ext.goneIf
import cash.z.ecc.android.ext.showCriticalMessage
import cash.z.ecc.android.ext.showCriticalProcessorError
import cash.z.ecc.android.ext.showScanFailure
import cash.z.ecc.android.ext.showUninitializedError
import cash.z.ecc.android.di.DependenciesHolder
import cash.z.ecc.android.ext.*
import cash.z.ecc.android.feedback.Feedback
import cash.z.ecc.android.feedback.FeedbackCoordinator
import cash.z.ecc.android.feedback.LaunchMetric
@ -71,7 +50,6 @@ import cash.z.ecc.android.feedback.Report.NonUserAction.FEEDBACK_STOPPED
import cash.z.ecc.android.feedback.Report.NonUserAction.SYNC_START
import cash.z.ecc.android.feedback.Report.Tap.COPY_ADDRESS
import cash.z.ecc.android.feedback.Report.Tap.COPY_TRANSPARENT_ADDRESS
import cash.z.ecc.android.sdk.Initializer
import cash.z.ecc.android.sdk.SdkSynchronizer
import cash.z.ecc.android.sdk.db.entity.ConfirmedTransaction
import cash.z.ecc.android.sdk.exception.CompactBlockProcessorException
@ -84,36 +62,27 @@ import cash.z.ecc.android.ui.util.MemoUtil
import cash.z.ecc.android.util.twig
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.snackbar.Snackbar
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch
import javax.inject.Inject
class MainActivity : AppCompatActivity(R.layout.main_activity) {
@Inject
lateinit var mainViewModel: MainViewModel
val mainViewModel: MainViewModel by viewModels()
@Inject
lateinit var feedback: Feedback
val feedback: Feedback = DependenciesHolder.feedback
@Inject
lateinit var feedbackCoordinator: FeedbackCoordinator
val feedbackCoordinator: FeedbackCoordinator = DependenciesHolder.feedbackCoordinator
@Inject
lateinit var clipboard: ClipboardManager
val clipboard: ClipboardManager = DependenciesHolder.clipboardManager
val isInitialized get() = ::synchronizerComponent.isInitialized
val historyViewModel: HistoryViewModel by viewModels()
val historyViewModel: HistoryViewModel by activityViewModel()
private var syncStarted = false
private val mediaPlayer: MediaPlayer = MediaPlayer()
private var snackbar: Snackbar? = null
private var dialog: Dialog? = null
private var ignoreScanFailure: Boolean = false
lateinit var component: MainActivitySubcomponent
lateinit var synchronizerComponent: SynchronizerSubcomponent
var navController: NavController? = null
private val navInitListeners: MutableList<() -> Unit> = mutableListOf()
@ -123,16 +92,10 @@ class MainActivity : AppCompatActivity(R.layout.main_activity) {
Manifest.permission.CAMERA
) == PackageManager.PERMISSION_GRANTED
val latestHeight: BlockHeight? get() = if (isInitialized) {
synchronizerComponent.synchronizer().latestHeight
} else {
null
}
val latestHeight: BlockHeight?
get() = DependenciesHolder.synchronizer.latestHeight
override fun onCreate(savedInstanceState: Bundle?) {
component = ZcashWalletApp.component.mainActivitySubcomponent().create(this).also {
it.inject(this)
}
lifecycleScope.launch {
feedback.start()
}
@ -217,9 +180,14 @@ class MainActivity : AppCompatActivity(R.layout.main_activity) {
navController?.popBackStack(destination, inclusive)
}
fun safeNavigate(navDirections: NavDirections) = safeNavigate(navDirections.actionId, navDirections.arguments, null)
fun safeNavigate(navDirections: NavDirections) =
safeNavigate(navDirections.actionId, navDirections.arguments, null)
fun safeNavigate(@IdRes destination: Int, args: Bundle? = null, extras: Navigator.Extras? = null) {
fun safeNavigate(
@IdRes destination: Int,
args: Bundle? = null,
extras: Navigator.Extras? = null
) {
if (navController == null) {
navInitListeners.add {
try {
@ -227,9 +195,9 @@ class MainActivity : AppCompatActivity(R.layout.main_activity) {
} catch (t: Throwable) {
twig(
"WARNING: during callback, did not navigate to destination: R.id.${
resources.getResourceEntryName(
destination
)
resources.getResourceEntryName(
destination
)
} due to: $t"
)
}
@ -240,29 +208,27 @@ class MainActivity : AppCompatActivity(R.layout.main_activity) {
} catch (t: Throwable) {
twig(
"WARNING: did not immediately navigate to destination: R.id.${
resources.getResourceEntryName(
destination
)
resources.getResourceEntryName(
destination
)
} due to: $t"
)
}
}
}
fun startSync(initializer: Initializer, isRestart: Boolean = false) {
fun startSync(isRestart: Boolean = false) {
twig("MainActivity.startSync")
if (!isInitialized || isRestart) {
if (!syncStarted || isRestart) {
syncStarted = true
mainViewModel.setLoading(true)
synchronizerComponent = ZcashWalletApp.component.synchronizerSubcomponent().create(
initializer
)
twig("Synchronizer component created")
feedback.report(SYNC_START)
synchronizerComponent.synchronizer().let { synchronizer ->
DependenciesHolder.synchronizer.let { synchronizer ->
synchronizer.onProcessorErrorHandler = ::onProcessorError
synchronizer.onChainErrorHandler = ::onChainError
synchronizer.onCriticalErrorHandler = ::onCriticalError
(synchronizer as SdkSynchronizer).processor.onScanMetricCompleteListener = ::onScanMetricComplete
(synchronizer as SdkSynchronizer).processor.onScanMetricCompleteListener =
::onScanMetricComplete
synchronizer.start(lifecycleScope)
mainViewModel.setSyncReady(true)
@ -278,8 +244,15 @@ class MainActivity : AppCompatActivity(R.layout.main_activity) {
val reportingThreshold = 100
if (isComplete) {
if (batchMetrics.cumulativeItems > reportingThreshold) {
val network = synchronizerComponent.synchronizer().network.networkName
reportAction(Report.Performance.ScanRate(network, batchMetrics.cumulativeItems.toInt(), batchMetrics.cumulativeTime, batchMetrics.cumulativeIps))
val network = DependenciesHolder.synchronizer.network.networkName
reportAction(
Report.Performance.ScanRate(
network,
batchMetrics.cumulativeItems.toInt(),
batchMetrics.cumulativeTime,
batchMetrics.cumulativeIps
)
)
}
}
}
@ -312,7 +285,11 @@ class MainActivity : AppCompatActivity(R.layout.main_activity) {
mainViewModel.setLoading(isLoading, message)
}
fun authenticate(description: String, title: String = getString(R.string.biometric_prompt_title), block: () -> Unit) {
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) {
twig("Authentication success with type: ${if (result.authenticationType == AUTHENTICATION_RESULT_TYPE_DEVICE_CREDENTIAL) "DEVICE_CREDENTIAL" else if (result.authenticationType == AUTHENTICATION_RESULT_TYPE_BIOMETRIC) "BIOMETRIC" else "UNKNOWN"} object: ${result.cryptoObject}")
@ -322,10 +299,12 @@ class MainActivity : AppCompatActivity(R.layout.main_activity) {
// but it doesn't hurt to hide the keyboard every time
hideKeyboard()
}
override fun onAuthenticationFailed() {
twig("Authentication failed!!!!")
showMessage("Authentication failed :(")
}
override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
twig("Authentication Error")
fun doNothing(message: String, interruptUser: Boolean = true) {
@ -339,7 +318,10 @@ class MainActivity : AppCompatActivity(R.layout.main_activity) {
ERROR_HW_NOT_PRESENT, ERROR_HW_UNAVAILABLE,
ERROR_NO_BIOMETRICS, ERROR_NO_DEVICE_CREDENTIAL -> {
twig("Warning: bypassing authentication because $errString [$errorCode]")
showMessage("Please enable screen lock on this device to add security here!", true)
showMessage(
"Please enable screen lock on this device to add security here!",
true
)
block()
}
ERROR_LOCKOUT -> doNothing("Too many attempts. Try again in 30s.")
@ -400,14 +382,14 @@ class MainActivity : AppCompatActivity(R.layout.main_activity) {
fun copyAddress(view: View? = null) {
reportTap(COPY_ADDRESS)
lifecycleScope.launch {
copyText(synchronizerComponent.synchronizer().getAddress(), "Address")
copyText(DependenciesHolder.synchronizer.getAddress(), "Address")
}
}
fun copyTransparentAddress(view: View? = null) {
reportTap(COPY_TRANSPARENT_ADDRESS)
lifecycleScope.launch {
copyText(synchronizerComponent.synchronizer().getTransparentAddress(), "T-Address")
copyText(DependenciesHolder.synchronizer.getTransparentAddress(), "T-Address")
}
}
@ -432,8 +414,9 @@ class MainActivity : AppCompatActivity(R.layout.main_activity) {
suspend fun isValidAddress(address: String): Boolean {
try {
return !synchronizerComponent.synchronizer().validateAddress(address).isNotValid
} catch (t: Throwable) { }
return !DependenciesHolder.synchronizer.validateAddress(address).isNotValid
} catch (t: Throwable) {
}
return false
}
@ -457,7 +440,11 @@ class MainActivity : AppCompatActivity(R.layout.main_activity) {
Toast.makeText(this, message, if (linger) Toast.LENGTH_LONG else Toast.LENGTH_SHORT).show()
}
fun showSnackbar(message: String, actionLabel: String = getString(android.R.string.ok), action: () -> Unit = {}): Snackbar {
fun showSnackbar(
message: String,
actionLabel: String = getString(android.R.string.ok),
action: () -> Unit = {}
): Snackbar {
return if (snackbar == null) {
val view = findViewById<View>(R.id.main_activity_container)
val snacks = Snackbar
@ -618,7 +605,8 @@ class MainActivity : AppCompatActivity(R.layout.main_activity) {
suspend fun getSender(transaction: ConfirmedTransaction?): String {
if (transaction == null) return getString(R.string.unknown)
return MemoUtil.findAddressInMemo(transaction, ::isValidAddress)?.toAbbreviatedAddress() ?: getString(R.string.unknown)
return MemoUtil.findAddressInMemo(transaction, ::isValidAddress)?.toAbbreviatedAddress()
?: getString(R.string.unknown)
}
suspend fun String?.validateAddress(): String? {

View File

@ -5,9 +5,8 @@ import cash.z.ecc.android.util.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() {
class MainViewModel : ViewModel() {
private val _loadingMessage = MutableStateFlow<String?>("\u23F3 Loading...")
private val _syncReady = MutableStateFlow(false)
val loadingMessage: StateFlow<String?> get() = _loadingMessage

View File

@ -3,13 +3,13 @@ package cash.z.ecc.android.ui.history
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import androidx.fragment.app.activityViewModels
import androidx.lifecycle.lifecycleScope
import androidx.paging.PagedList
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import cash.z.ecc.android.R
import cash.z.ecc.android.databinding.FragmentHistoryBinding
import cash.z.ecc.android.di.viewmodel.activityViewModel
import cash.z.ecc.android.ext.*
import cash.z.ecc.android.feedback.Report
import cash.z.ecc.android.feedback.Report.Tap.HISTORY_BACK
@ -25,7 +25,7 @@ import kotlinx.coroutines.launch
class HistoryFragment : BaseFragment<FragmentHistoryBinding>() {
override val screen = Report.Screen.HISTORY
private val viewModel: HistoryViewModel by activityViewModel()
private val viewModel: HistoryViewModel by activityViewModels()
private lateinit var transactionAdapter: TransactionAdapter<ConfirmedTransaction>

View File

@ -5,7 +5,7 @@ import androidx.annotation.StringRes
import androidx.lifecycle.ViewModel
import cash.z.ecc.android.R
import cash.z.ecc.android.ZcashWalletApp
import cash.z.ecc.android.ext.Const
import cash.z.ecc.android.di.DependenciesHolder
import cash.z.ecc.android.ext.WalletZecFormmatter
import cash.z.ecc.android.ext.toAppString
import cash.z.ecc.android.ext.toAppStringFormatted
@ -21,17 +21,12 @@ import cash.z.ecc.android.ui.util.toUtf8Memo
import cash.z.ecc.android.util.twig
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.map
import javax.inject.Inject
import javax.inject.Named
class HistoryViewModel @Inject constructor() : ViewModel() {
class HistoryViewModel : ViewModel() {
@Inject
lateinit var synchronizer: Synchronizer
private val synchronizer: Synchronizer = DependenciesHolder.synchronizer
@Inject
@Named(Const.Name.APP_PREFS)
lateinit var prefs: LockBox
val prefs: LockBox = DependenciesHolder.prefs
val selectedTransaction = MutableStateFlow<ConfirmedTransaction?>(null)
val uiModels = selectedTransaction.map { it.toUiModel() }
@ -70,82 +65,95 @@ class HistoryViewModel @Inject constructor() : ViewModel() {
var txId: String? = null
)
private suspend fun ConfirmedTransaction?.toUiModel(latestHeight: Int? = null): UiModel = UiModel().apply {
this@toUiModel.let { tx ->
txId = toTxId(tx?.rawTransactionId)
isInbound = when {
!(tx?.toAddress.isNullOrEmpty()) -> false
tx != null && tx.toAddress.isNullOrEmpty() && tx.value > 0L && tx.minedHeight > 0 -> true
else -> null
}
isMined = tx?.minedHeight != null && tx.minedHeight > synchronizer.network.saplingActivationHeight.value
topValue = if (tx == null) "" else "\$${WalletZecFormmatter.toZecStringFull(tx.valueInZatoshi)}"
minedHeight = String.format("%,d", tx?.minedHeight ?: 0)
val flags =
DateUtils.FORMAT_SHOW_DATE or DateUtils.FORMAT_SHOW_YEAR or DateUtils.FORMAT_ABBREV_MONTH
timestamp = if (tx == null || tx.blockTimeInSeconds <= 0) getString(R.string.transaction_timestamp_unavailable) else DateUtils.getRelativeDateTimeString(
ZcashWalletApp.instance,
tx.blockTimeInSeconds * 1000,
DateUtils.SECOND_IN_MILLIS,
DateUtils.WEEK_IN_MILLIS,
flags
).toString()
private suspend fun ConfirmedTransaction?.toUiModel(latestHeight: Int? = null): UiModel =
UiModel().apply {
this@toUiModel.let { tx ->
txId = toTxId(tx?.rawTransactionId)
isInbound = when {
!(tx?.toAddress.isNullOrEmpty()) -> false
tx != null && tx.toAddress.isNullOrEmpty() && tx.value > 0L && tx.minedHeight > 0 -> true
else -> null
}
isMined =
tx?.minedHeight != null && tx.minedHeight > synchronizer.network.saplingActivationHeight.value
topValue =
if (tx == null) "" else "\$${WalletZecFormmatter.toZecStringFull(tx.valueInZatoshi)}"
minedHeight = String.format("%,d", tx?.minedHeight ?: 0)
val flags =
DateUtils.FORMAT_SHOW_DATE or DateUtils.FORMAT_SHOW_YEAR or DateUtils.FORMAT_ABBREV_MONTH
timestamp =
if (tx == null || tx.blockTimeInSeconds <= 0) getString(R.string.transaction_timestamp_unavailable) else DateUtils.getRelativeDateTimeString(
ZcashWalletApp.instance,
tx.blockTimeInSeconds * 1000,
DateUtils.SECOND_IN_MILLIS,
DateUtils.WEEK_IN_MILLIS,
flags
).toString()
// memo logic
val txMemo = tx?.memo.toUtf8Memo()
if (!txMemo.isEmpty()) {
memo = txMemo
}
// memo logic
val txMemo = tx?.memo.toUtf8Memo()
if (!txMemo.isEmpty()) {
memo = txMemo
}
// confirmation logic
// TODO: clean all of this up and remove/improve reliance on `isSufficientlyOld` function. Also, add a constant for the number of confirmations we expect.
tx?.let {
val isMined = it.blockTimeInSeconds != 0L
if (isMined) {
val hasLatestHeight = latestHeight != null && latestHeight > synchronizer.network.saplingActivationHeight.value
if (it.minedHeight > 0 && hasLatestHeight) {
val confirmations = latestHeight!! - it.minedHeight + 1
confirmation = if (confirmations >= 10) getString(R.string.transaction_status_confirmed) else "$confirmations ${getString(
R.string.transaction_status_confirming
)}"
} else {
if (!hasLatestHeight && isSufficientlyOld(tx)) {
twig("Warning: could not load latestheight from server to determine confirmations but this transaction is mined and old enough to be considered confirmed")
confirmation = getString(R.string.transaction_status_confirmed)
// confirmation logic
// TODO: clean all of this up and remove/improve reliance on `isSufficientlyOld` function. Also, add a constant for the number of confirmations we expect.
tx?.let {
val isMined = it.blockTimeInSeconds != 0L
if (isMined) {
val hasLatestHeight =
latestHeight != null && latestHeight > synchronizer.network.saplingActivationHeight.value
if (it.minedHeight > 0 && hasLatestHeight) {
val confirmations = latestHeight!! - it.minedHeight + 1
confirmation =
if (confirmations >= 10) getString(R.string.transaction_status_confirmed) else "$confirmations ${
getString(
R.string.transaction_status_confirming
)
}"
} else {
twig("Warning: could not determine confirmation text value so it will be left null!")
confirmation = getString(R.string.transaction_confirmation_count_unavailable)
if (!hasLatestHeight && isSufficientlyOld(tx)) {
twig("Warning: could not load latestheight from server to determine confirmations but this transaction is mined and old enough to be considered confirmed")
confirmation = getString(R.string.transaction_status_confirmed)
} else {
twig("Warning: could not determine confirmation text value so it will be left null!")
confirmation =
getString(R.string.transaction_confirmation_count_unavailable)
}
}
} else {
confirmation = getString(R.string.transaction_status_pending)
}
} else {
confirmation = getString(R.string.transaction_status_pending)
}
}
when (isInbound) {
true -> {
topLabel = getString(R.string.transaction_story_inbound)
bottomLabel = getString(R.string.transaction_story_inbound_total)
bottomValue = "\$${WalletZecFormmatter.toZecStringFull(tx?.valueInZatoshi)}"
iconRotation = 315f
source = getString(R.string.transaction_story_to_shielded)
address = MemoUtil.findAddressInMemo(tx, (synchronizer as SdkSynchronizer)::isValidAddress)
}
false -> {
topLabel = getString(R.string.transaction_story_outbound)
bottomLabel = getString(R.string.transaction_story_outbound_total)
bottomValue = "\$${WalletZecFormmatter.toZecStringFull(Zatoshi((tx?.valueInZatoshi?.value ?: 0) + ZcashSdk.MINERS_FEE.value))}"
iconRotation = 135f
fee = "+ 0.00001 network fee"
source = getString(R.string.transaction_story_from_shielded)
address = tx?.toAddress
}
null -> {
twig("Error: transaction appears to be invalid.")
when (isInbound) {
true -> {
topLabel = getString(R.string.transaction_story_inbound)
bottomLabel = getString(R.string.transaction_story_inbound_total)
bottomValue = "\$${WalletZecFormmatter.toZecStringFull(tx?.valueInZatoshi)}"
iconRotation = 315f
source = getString(R.string.transaction_story_to_shielded)
address = MemoUtil.findAddressInMemo(
tx,
(synchronizer as SdkSynchronizer)::isValidAddress
)
}
false -> {
topLabel = getString(R.string.transaction_story_outbound)
bottomLabel = getString(R.string.transaction_story_outbound_total)
bottomValue =
"\$${WalletZecFormmatter.toZecStringFull(Zatoshi((tx?.valueInZatoshi?.value ?: 0) + ZcashSdk.MINERS_FEE.value))}"
iconRotation = 135f
fee = "+ 0.00001 network fee"
source = getString(R.string.transaction_story_from_shielded)
address = tx?.toAddress
}
null -> {
twig("Error: transaction appears to be invalid.")
}
}
}
}
}
private fun getString(@StringRes id: Int) = id.toAppString()
private fun getString(@StringRes id: Int, vararg args: Any) = id.toAppStringFormatted(args)
@ -166,6 +174,6 @@ class HistoryViewModel @Inject constructor() : ViewModel() {
val threshold = 75 * 1000 * 25 // approx 25 blocks
val delta = System.currentTimeMillis() / 1000L - tx.blockTimeInSeconds
return tx.minedHeight > synchronizer.network.saplingActivationHeight.value &&
delta < threshold
delta < threshold
}
}

View File

@ -8,34 +8,23 @@ import android.text.method.ScrollingMovementMethod
import android.view.LayoutInflater
import android.view.View
import androidx.core.view.ViewCompat
import androidx.fragment.app.activityViewModels
import androidx.lifecycle.lifecycleScope
import androidx.transition.ChangeBounds
import androidx.transition.ChangeClipBounds
import androidx.transition.ChangeTransform
import androidx.transition.Transition
import androidx.transition.TransitionSet
import androidx.transition.*
import cash.z.ecc.android.R
import cash.z.ecc.android.databinding.FragmentTransactionBinding
import cash.z.ecc.android.di.viewmodel.activityViewModel
import cash.z.ecc.android.ext.Const
import cash.z.ecc.android.ext.gone
import cash.z.ecc.android.ext.invisible
import cash.z.ecc.android.ext.onClickNavBack
import cash.z.ecc.android.ext.toAppColor
import cash.z.ecc.android.ext.toColoredSpan
import cash.z.ecc.android.ext.visible
import cash.z.ecc.android.ext.*
import cash.z.ecc.android.feedback.Report
import cash.z.ecc.android.sdk.ext.toAbbreviatedAddress
import cash.z.ecc.android.ui.base.BaseFragment
import cash.z.ecc.android.ui.history.HistoryViewModel.UiModel
import cash.z.ecc.android.util.twig
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
class TransactionFragment : BaseFragment<FragmentTransactionBinding>() {
override val screen = Report.Screen.TRANSACTION
private val viewModel: HistoryViewModel by activityViewModel()
private val viewModel: HistoryViewModel by activityViewModels()
var isMemoExpanded: Boolean = false
@ -74,8 +63,14 @@ class TransactionFragment : BaseFragment<FragmentTransactionBinding>() {
super.onViewCreated(view, savedInstanceState)
binding.apply {
ViewCompat.setTransitionName(topBoxValue, "test_amount_anim_${viewModel.selectedTransaction.value?.id}")
ViewCompat.setTransitionName(topBoxBackground, "test_bg_anim_${viewModel.selectedTransaction.value?.id}")
ViewCompat.setTransitionName(
topBoxValue,
"test_amount_anim_${viewModel.selectedTransaction.value?.id}"
)
ViewCompat.setTransitionName(
topBoxBackground,
"test_bg_anim_${viewModel.selectedTransaction.value?.id}"
)
backButtonHitArea.onClickNavBack { tapped(Report.Tap.TRANSACTION_BACK) }
lifecycleScope.launch {
@ -114,10 +109,19 @@ class TransactionFragment : BaseFragment<FragmentTransactionBinding>() {
buttonExplore.setOnClickListener(exploreOnClick)
textBlockHeight.setOnClickListener(exploreOnClick)
uiModel.fee?.let { subwaySpotFee.visible(); subwayLabelFee.visible(); subwayLabelFee.text = it }
uiModel.source?.let { subwaySpotSource.visible(); subwayLabelSource.visible(); subwayLabelSource.text = it }
uiModel.toAddressLabel()?.let { subwaySpotAddress.visible(); subwayLabelAddress.visible(); subwayLabelAddress.text = it }
uiModel.toAddressClickListener()?.let { subwayLabelAddress.setOnClickListener(it) }
uiModel.fee?.let {
subwaySpotFee.visible(); subwayLabelFee.visible(); subwayLabelFee.text = it
}
uiModel.source?.let {
subwaySpotSource.visible(); subwayLabelSource.visible(); subwayLabelSource.text =
it
}
uiModel.toAddressLabel()?.let {
subwaySpotAddress.visible(); subwayLabelAddress.visible(); subwayLabelAddress.text =
it
}
uiModel.toAddressClickListener()
?.let { subwayLabelAddress.setOnClickListener(it) }
// TODO: remove logic from sections below and add more fields or extension functions to UiModel
uiModel.confirmation?.let {
@ -131,9 +135,24 @@ class TransactionFragment : BaseFragment<FragmentTransactionBinding>() {
}
uiModel.memo?.let {
hitAreaMemoSubway.setOnClickListener { _ -> onToggleMemo(!isMemoExpanded, it) }
hitAreaMemoIcon.setOnClickListener { _ -> onToggleMemo(!isMemoExpanded, it) }
subwayLabelMemo.setOnClickListener { _ -> onToggleMemo(!isMemoExpanded, it) }
hitAreaMemoSubway.setOnClickListener { _ ->
onToggleMemo(
!isMemoExpanded,
it
)
}
hitAreaMemoIcon.setOnClickListener { _ ->
onToggleMemo(
!isMemoExpanded,
it
)
}
subwayLabelMemo.setOnClickListener { _ ->
onToggleMemo(
!isMemoExpanded,
it
)
}
subwayLabelMemo.setOnLongClickListener { _ ->
mainActivity?.copyText(it, "Memo")
true
@ -158,7 +177,8 @@ class TransactionFragment : BaseFragment<FragmentTransactionBinding>() {
binding.subwayLabelMemo.invalidate()
// don't impede the ability to scroll
binding.groupMemoIcon.gone()
binding.subwayLabelMemo.backgroundTintList = ColorStateList.valueOf(R.color.tx_text_light_dimmed.toAppColor())
binding.subwayLabelMemo.backgroundTintList =
ColorStateList.valueOf(R.color.tx_text_light_dimmed.toAppColor())
binding.subwaySpotMemoContent.colorFilter = invertingMatrix
binding.subwaySpotMemoContent.rotation = 90.0f
} else {
@ -167,7 +187,8 @@ class TransactionFragment : BaseFragment<FragmentTransactionBinding>() {
binding.subwayLabelMemo.invalidate()
twig("setting memo text to: with a memo")
binding.groupMemoIcon.visible()
binding.subwayLabelMemo.backgroundTintList = ColorStateList.valueOf(R.color.tx_primary.toAppColor())
binding.subwayLabelMemo.backgroundTintList =
ColorStateList.valueOf(R.color.tx_primary.toAppColor())
binding.subwaySpotMemoContent.colorFilter = null
binding.subwaySpotMemoContent.rotation = 0.0f
}
@ -193,7 +214,7 @@ class TransactionFragment : BaseFragment<FragmentTransactionBinding>() {
R.string.transaction_prefix_to
}
)
return "$prefix ${address?.toAbbreviatedAddress() ?: "Unknown" }".let {
return "$prefix ${address?.toAbbreviatedAddress() ?: "Unknown"}".let {
it.toColoredSpan(R.color.tx_text_light_dimmed, if (address == null) it else prefix)
}
}

View File

@ -4,13 +4,13 @@ import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.widget.Toast
import androidx.fragment.app.viewModels
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import cash.z.ecc.android.R
import cash.z.ecc.android.ZcashWalletApp
import cash.z.ecc.android.databinding.FragmentBalanceDetailBinding
import cash.z.ecc.android.di.viewmodel.viewModel
import cash.z.ecc.android.ext.goneIf
import cash.z.ecc.android.ext.onClickNavBack
import cash.z.ecc.android.ext.toAppColor
@ -26,7 +26,7 @@ import kotlinx.coroutines.launch
class BalanceDetailFragment : BaseFragment<FragmentBalanceDetailBinding>() {
private val viewModel: BalanceDetailViewModel by viewModel()
private val viewModel: BalanceDetailViewModel by viewModels()
private var lastSignal: BlockHeight? = null
override fun inflate(inflater: LayoutInflater): FragmentBalanceDetailBinding =
@ -125,7 +125,10 @@ class BalanceDetailFragment : BaseFragment<FragmentBalanceDetailBinding>() {
binding.textStatus.text = status.toStatus()
if (status.missingBlocks > 100) {
binding.textBlockHeightPrefix.text = "Processing "
binding.textBlockHeight.text = String.format("%,d", status.info.lastScannedHeight?.value ?: 0) + " of " + String.format("%,d", status.info.networkBlockHeight?.value ?: 0)
binding.textBlockHeight.text = String.format(
"%,d",
status.info.lastScannedHeight?.value ?: 0
) + " of " + String.format("%,d", status.info.networkBlockHeight?.value ?: 0)
} else {
status.info.lastScannedHeight.let { height ->
if (height == null) {
@ -133,7 +136,8 @@ class BalanceDetailFragment : BaseFragment<FragmentBalanceDetailBinding>() {
binding.textBlockHeight.text = ""
} else {
binding.textBlockHeightPrefix.text = "Balances as of block "
binding.textBlockHeight.text = String.format("%,d", status.info.lastScannedHeight?.value ?: 0)
binding.textBlockHeight.text =
String.format("%,d", status.info.lastScannedHeight?.value ?: 0)
sendNewBlockSignal(status.info.lastScannedHeight)
}
}
@ -185,13 +189,29 @@ class BalanceDetailFragment : BaseFragment<FragmentBalanceDetailBinding>() {
status += when {
hasPendingTransparentBalance && hasPendingShieldedBalance -> {
"Awaiting ${pendingShieldedBalance.convertZatoshiToZecString(8)} ${ZcashWalletApp.instance.getString(R.string.symbol)} in shielded funds and ${pendingTransparentBalance.convertZatoshiToZecString(8)} ${ZcashWalletApp.instance.getString(R.string.symbol)} in transparent funds"
"Awaiting ${pendingShieldedBalance.convertZatoshiToZecString(8)} ${
ZcashWalletApp.instance.getString(
R.string.symbol
)
} in shielded funds and ${pendingTransparentBalance.convertZatoshiToZecString(8)} ${
ZcashWalletApp.instance.getString(
R.string.symbol
)
} in transparent funds"
}
hasPendingShieldedBalance -> {
"Awaiting ${pendingShieldedBalance.convertZatoshiToZecString(8)} ${ZcashWalletApp.instance.getString(R.string.symbol)} in shielded funds"
"Awaiting ${pendingShieldedBalance.convertZatoshiToZecString(8)} ${
ZcashWalletApp.instance.getString(
R.string.symbol
)
} in shielded funds"
}
hasPendingTransparentBalance -> {
"Awaiting ${pendingTransparentBalance.convertZatoshiToZecString(8)} ${ZcashWalletApp.instance.getString(R.string.symbol)} in transparent funds"
"Awaiting ${pendingTransparentBalance.convertZatoshiToZecString(8)} ${
ZcashWalletApp.instance.getString(
R.string.symbol
)
} in transparent funds"
}
else -> ""
}

View File

@ -1,6 +1,7 @@
package cash.z.ecc.android.ui.home
import androidx.lifecycle.ViewModel
import cash.z.ecc.android.di.DependenciesHolder
import cash.z.ecc.android.lockbox.LockBox
import cash.z.ecc.android.sdk.Synchronizer
import cash.z.ecc.android.sdk.block.CompactBlockProcessor
@ -14,15 +15,12 @@ import cash.z.ecc.android.sdk.model.WalletBalance
import cash.z.ecc.android.sdk.model.Zatoshi
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combineTransform
import javax.inject.Inject
class BalanceDetailViewModel @Inject constructor() : ViewModel() {
class BalanceDetailViewModel : ViewModel() {
@Inject
lateinit var synchronizer: Synchronizer
private val synchronizer: Synchronizer = DependenciesHolder.synchronizer
@Inject
lateinit var lockBox: LockBox
private val lockBox: LockBox = DependenciesHolder.lockBox
var showAvailable: Boolean = true
set(value) {
@ -59,7 +57,8 @@ class BalanceDetailViewModel @Inject constructor() : ViewModel() {
) {
/** Whether to make calculations based on total or available zatoshi */
val canAutoShield: Boolean = (transparentBalance?.available?.value ?: 0L) > ZcashSdk.MINERS_FEE.value
val canAutoShield: Boolean =
(transparentBalance?.available?.value ?: 0L) > ZcashSdk.MINERS_FEE.value
val balanceShielded: String
get() {
@ -75,16 +74,25 @@ class BalanceDetailViewModel @Inject constructor() : ViewModel() {
val balanceTotal: String
get() {
return if (showAvailable) ((shieldedBalance?.available ?: Zatoshi(0)) + (transparentBalance?.available ?: Zatoshi(0))).toDisplay()
else ((shieldedBalance?.total ?: Zatoshi(0)) + (transparentBalance?.total ?: Zatoshi(0))).toDisplay()
return if (showAvailable) ((shieldedBalance?.available
?: Zatoshi(0)) + (transparentBalance?.available ?: Zatoshi(0))).toDisplay()
else ((shieldedBalance?.total ?: Zatoshi(0)) + (transparentBalance?.total
?: Zatoshi(0))).toDisplay()
}
val paddedShielded get() = pad(balanceShielded)
val paddedTransparent get() = pad(balanceTransparent)
val paddedTotal get() = pad(balanceTotal)
val maxLength get() = maxOf(balanceShielded.length, balanceTransparent.length, balanceTotal.length)
val hasPending = (null != shieldedBalance && shieldedBalance.available != shieldedBalance.total) ||
(null != transparentBalance && transparentBalance.available != transparentBalance.total)
val maxLength
get() = maxOf(
balanceShielded.length,
balanceTransparent.length,
balanceTotal.length
)
val hasPending =
(null != shieldedBalance && shieldedBalance.available != shieldedBalance.total) ||
(null != transparentBalance && transparentBalance.available != transparentBalance.total)
private fun Zatoshi?.toDisplay(): String {
return this?.convertZatoshiToZecString(8, 8) ?: "0"
}
@ -109,7 +117,8 @@ class BalanceDetailViewModel @Inject constructor() : ViewModel() {
val pending: List<PendingTransaction>,
val info: CompactBlockProcessor.ProcessorInfo,
) {
val pendingUnconfirmed = pending.filter { it.isSubmitSuccess() && it.isMined() && !it.isConfirmed(info.lastScannedHeight) }
val pendingUnconfirmed =
pending.filter { it.isSubmitSuccess() && it.isMined() && !it.isConfirmed(info.lastScannedHeight) }
val pendingUnmined = pending.filter { it.isSubmitSuccess() && !it.isMined() }
val pendingShieldedBalance = balances.shieldedBalance?.pending
val pendingTransparentBalance = balances.transparentBalance?.pending
@ -117,17 +126,21 @@ class BalanceDetailViewModel @Inject constructor() : ViewModel() {
val hasUnmined = pendingUnmined.isNotEmpty()
val hasPendingShieldedBalance = (pendingShieldedBalance?.value ?: 0L) > 0L
val hasPendingTransparentBalance = (pendingTransparentBalance?.value ?: 0L) > 0L
val missingBlocks = ((info.networkBlockHeight?.value ?: 0) - (info.lastScannedHeight?.value ?: 0)).coerceAtLeast(0)
val missingBlocks = ((info.networkBlockHeight?.value ?: 0) - (info.lastScannedHeight?.value
?: 0)).coerceAtLeast(0)
private fun PendingTransaction.isConfirmed(networkBlockHeight: BlockHeight?): Boolean {
return networkBlockHeight?.let {
isMined() && (it.value - minedHeight + 1) > 10 // fix: plus 1 because the mined block counts as the FIRST confirmation
isMined() && (it.value - minedHeight + 1) > 10 // fix: plus 1 because the mined block counts as the FIRST confirmation
} ?: false
}
fun remainingConfirmations(confirmationsRequired: Int = 10) =
pendingUnconfirmed
.map { confirmationsRequired - ((info.lastScannedHeight?.value ?: -1) - it.minedHeight + 1) } // fix: plus 1 because the mined block counts as the FIRST confirmation
.map {
confirmationsRequired - ((info.lastScannedHeight?.value
?: -1) - it.minedHeight + 1)
} // fix: plus 1 because the mined block counts as the FIRST confirmation
.filter { it > 0 }
.sortedDescending()
}

View File

@ -8,45 +8,26 @@ import android.view.View
import android.widget.TextView
import android.widget.Toast
import androidx.appcompat.content.res.AppCompatResources
import androidx.fragment.app.activityViewModels
import androidx.fragment.app.viewModels
import androidx.lifecycle.lifecycleScope
import cash.z.ecc.android.R
import cash.z.ecc.android.ZcashWalletApp
import cash.z.ecc.android.databinding.DialogSolicitFeedbackRatingBinding
import cash.z.ecc.android.databinding.FragmentHomeBinding
import cash.z.ecc.android.di.viewmodel.activityViewModel
import cash.z.ecc.android.di.viewmodel.viewModel
import cash.z.ecc.android.ext.WalletZecFormmatter
import cash.z.ecc.android.ext.disabledIf
import cash.z.ecc.android.ext.goneIf
import cash.z.ecc.android.ext.invisibleIf
import cash.z.ecc.android.ext.onClickNavTo
import cash.z.ecc.android.ext.requireApplicationContext
import cash.z.ecc.android.ext.showSharedLibraryCriticalError
import cash.z.ecc.android.ext.toAppColor
import cash.z.ecc.android.ext.toColoredSpan
import cash.z.ecc.android.ext.transparentIf
import cash.z.ecc.android.ext.*
import cash.z.ecc.android.feedback.Report
import cash.z.ecc.android.feedback.Report.Tap.HOME_BALANCE_DETAIL
import cash.z.ecc.android.feedback.Report.Tap.HOME_CLEAR_AMOUNT
import cash.z.ecc.android.feedback.Report.Tap.HOME_FUND_NOW
import cash.z.ecc.android.feedback.Report.Tap.HOME_HISTORY
import cash.z.ecc.android.feedback.Report.Tap.HOME_PROFILE
import cash.z.ecc.android.feedback.Report.Tap.HOME_RECEIVE
import cash.z.ecc.android.feedback.Report.Tap.HOME_SEND
import cash.z.ecc.android.feedback.Report.Tap.*
import cash.z.ecc.android.preference.Preferences
import cash.z.ecc.android.preference.model.get
import cash.z.ecc.android.sdk.Synchronizer.Status.DISCONNECTED
import cash.z.ecc.android.sdk.Synchronizer.Status.STOPPED
import cash.z.ecc.android.sdk.Synchronizer.Status.SYNCED
import cash.z.ecc.android.sdk.Synchronizer.Status.*
import cash.z.ecc.android.sdk.ext.convertZatoshiToZecString
import cash.z.ecc.android.sdk.ext.convertZecToZatoshi
import cash.z.ecc.android.sdk.ext.onFirstWith
import cash.z.ecc.android.sdk.ext.safelyConvertToBigDecimal
import cash.z.ecc.android.sdk.model.Zatoshi
import cash.z.ecc.android.ui.base.BaseFragment
import cash.z.ecc.android.ui.home.HomeFragment.BannerAction.CANCEL
import cash.z.ecc.android.ui.home.HomeFragment.BannerAction.CLEAR
import cash.z.ecc.android.ui.home.HomeFragment.BannerAction.FUND_NOW
import cash.z.ecc.android.ui.home.HomeFragment.BannerAction.*
import cash.z.ecc.android.ui.send.AutoShieldFragment
import cash.z.ecc.android.ui.send.SendViewModel
import cash.z.ecc.android.ui.setup.WalletSetupViewModel
@ -66,13 +47,13 @@ import kotlinx.coroutines.launch
class HomeFragment : BaseFragment<FragmentHomeBinding>() {
override val screen = Report.Screen.HOME
private val walletSetup: WalletSetupViewModel by activityViewModels()
private val sendViewModel: SendViewModel by activityViewModels()
private val viewModel: HomeViewModel by viewModels()
private lateinit var numberPad: List<TextView>
private lateinit var uiModel: HomeViewModel.UiModel
private val walletSetup: WalletSetupViewModel by activityViewModel(false)
private val sendViewModel: SendViewModel by activityViewModel()
private val viewModel: HomeViewModel by viewModel()
lateinit var snake: MagicSnakeLoader
override fun inflate(inflater: LayoutInflater): FragmentHomeBinding =
@ -101,7 +82,8 @@ class HomeFragment : BaseFragment<FragmentHomeBinding>() {
twig("Previous wallet found. Re-opening it.")
mainActivity?.setLoading(true)
try {
mainActivity?.startSync(walletSetup.openStoredWallet())
walletSetup.openStoredWallet()
mainActivity?.startSync()
} catch (e: UnsatisfiedLinkError) {
mainActivity?.showSharedLibraryCriticalError(e)
}
@ -130,8 +112,16 @@ class HomeFragment : BaseFragment<FragmentHomeBinding>() {
)
hitAreaProfile.onClickNavTo(R.id.action_nav_home_to_nav_profile) { tapped(HOME_PROFILE) }
textHistory.onClickNavTo(R.id.action_nav_home_to_nav_history) { tapped(HOME_HISTORY) }
textSendAmount.onClickNavTo(R.id.action_nav_home_to_nav_balance_detail) { tapped(HOME_BALANCE_DETAIL) }
hitAreaBalance.onClickNavTo(R.id.action_nav_home_to_nav_balance_detail) { tapped(HOME_BALANCE_DETAIL) }
textSendAmount.onClickNavTo(R.id.action_nav_home_to_nav_balance_detail) {
tapped(
HOME_BALANCE_DETAIL
)
}
hitAreaBalance.onClickNavTo(R.id.action_nav_home_to_nav_balance_detail) {
tapped(
HOME_BALANCE_DETAIL
)
}
hitAreaReceive.onClickNavTo(R.id.action_nav_home_to_nav_receive) { tapped(HOME_RECEIVE) }
textBannerAction.setOnClickListener {
@ -157,7 +147,14 @@ class HomeFragment : BaseFragment<FragmentHomeBinding>() {
if (::uiModel.isInitialized) {
twig("uiModel exists! it has pendingSend=${uiModel.pendingSend} ZEC while the sendViewModel=${sendViewModel.zatoshiAmount} zats")
// if the model already existed, cool but let the sendViewModel be the source of truth for the amount
onModelUpdated(null, uiModel.copy(pendingSend = WalletZecFormmatter.toZecStringFull(sendViewModel.zatoshiAmount ?: Zatoshi(0L))))
onModelUpdated(
null,
uiModel.copy(
pendingSend = WalletZecFormmatter.toZecStringFull(
sendViewModel.zatoshiAmount ?: Zatoshi(0L)
)
)
)
}
}
@ -241,7 +238,9 @@ class HomeFragment : BaseFragment<FragmentHomeBinding>() {
val sendText = when {
uiModel.status == DISCONNECTED -> getString(R.string.home_button_send_disconnected)
uiModel.isSynced -> if (uiModel.hasFunds) getString(R.string.home_button_send_has_funds) else getString(R.string.home_button_send_no_funds)
uiModel.isSynced -> if (uiModel.hasFunds) getString(R.string.home_button_send_has_funds) else getString(
R.string.home_button_send_no_funds
)
uiModel.status == STOPPED -> getString(R.string.home_button_send_idle)
uiModel.isDownloading -> {
when (snake.downloadProgress) {
@ -263,8 +262,16 @@ class HomeFragment : BaseFragment<FragmentHomeBinding>() {
binding.buttonSendAmount.text = sendText
twig("Send button set to: $sendText")
val resId = if (uiModel.isSynced) R.color.selector_button_text_dark else R.color.selector_button_text_light
context?.let { binding.buttonSendAmount.setTextColor(AppCompatResources.getColorStateList(it, resId)) }
val resId =
if (uiModel.isSynced) R.color.selector_button_text_dark else R.color.selector_button_text_light
context?.let {
binding.buttonSendAmount.setTextColor(
AppCompatResources.getColorStateList(
it,
resId
)
)
}
binding.lottieButtonLoading.invisibleIf(uiModel.isDisconnected)
}
@ -276,14 +283,28 @@ class HomeFragment : BaseFragment<FragmentHomeBinding>() {
binding.textSendAmount.text = "\$$amount".toColoredSpan(R.color.text_light_dimmed, "$")
if (updateModel) {
sendViewModel.zatoshiAmount = amount.safelyConvertToBigDecimal().convertZecToZatoshi()
twig("dBUG: updating model. converting: $amount\tresult: ${sendViewModel.zatoshiAmount}\tprint: ${WalletZecFormmatter.toZecStringFull(sendViewModel.zatoshiAmount)}")
twig(
"dBUG: updating model. converting: $amount\tresult: ${sendViewModel.zatoshiAmount}\tprint: ${
WalletZecFormmatter.toZecStringFull(
sendViewModel.zatoshiAmount
)
}"
)
}
binding.buttonSendAmount.disabledIf(amount == "0")
}
fun setAvailable(availableBalance: Zatoshi?, totalBalance: Zatoshi?, availableTransparentBalance: Zatoshi?, unminedCount: Int = 0) {
fun setAvailable(
availableBalance: Zatoshi?,
totalBalance: Zatoshi?,
availableTransparentBalance: Zatoshi?,
unminedCount: Int = 0
) {
val missingBalance = availableBalance == null
val availableString = if (missingBalance) getString(R.string.home_button_send_updating) else WalletZecFormmatter.toZecStringFull(availableBalance)
val availableString =
if (missingBalance) getString(R.string.home_button_send_updating) else WalletZecFormmatter.toZecStringFull(
availableBalance
)
binding.textBalanceAvailable.text = availableString
binding.textBalanceAvailable.transparentIf(missingBalance)
binding.labelBalance.transparentIf(missingBalance)
@ -292,9 +313,13 @@ class HomeFragment : BaseFragment<FragmentHomeBinding>() {
text = when {
unminedCount > 0 -> "(excludes $unminedCount unconfirmed ${if (unminedCount > 1) "transactions" else "transaction"})"
availableBalance != null && totalBalance != null && (availableBalance.value < totalBalance.value) -> {
val change = WalletZecFormmatter.toZecStringFull(totalBalance - availableBalance)
val change =
WalletZecFormmatter.toZecStringFull(totalBalance - availableBalance)
val symbol = getString(R.string.symbol)
"(${getString(R.string.home_banner_expecting)} +$change $symbol)".toColoredSpan(R.color.text_light, "+$change")
"(${getString(R.string.home_banner_expecting)} +$change $symbol)".toColoredSpan(
R.color.text_light,
"+$change"
)
}
else -> getString(R.string.home_instruction_enter_amount)
}
@ -344,11 +369,21 @@ class HomeFragment : BaseFragment<FragmentHomeBinding>() {
append("${maybeComma()}processorInfo=ProcessorInfo(")
val startLength = length
fun innerComma() = if (length > startLength) ", " else ""
if (old.processorInfo.networkBlockHeight != new.processorInfo.networkBlockHeight) append("networkBlockHeight=${new.processorInfo.networkBlockHeight}")
if (old.processorInfo.lastScannedHeight != new.processorInfo.lastScannedHeight) append("${innerComma()}lastScannedHeight=${new.processorInfo.lastScannedHeight}")
if (old.processorInfo.lastDownloadedHeight != new.processorInfo.lastDownloadedHeight) append("${innerComma()}lastDownloadedHeight=${new.processorInfo.lastDownloadedHeight}")
if (old.processorInfo.lastDownloadRange != new.processorInfo.lastDownloadRange) append("${innerComma()}lastDownloadRange=${new.processorInfo.lastDownloadRange}")
if (old.processorInfo.lastScanRange != new.processorInfo.lastScanRange) append("${innerComma()}lastScanRange=${new.processorInfo.lastScanRange}")
if (old.processorInfo.networkBlockHeight != new.processorInfo.networkBlockHeight) append(
"networkBlockHeight=${new.processorInfo.networkBlockHeight}"
)
if (old.processorInfo.lastScannedHeight != new.processorInfo.lastScannedHeight) append(
"${innerComma()}lastScannedHeight=${new.processorInfo.lastScannedHeight}"
)
if (old.processorInfo.lastDownloadedHeight != new.processorInfo.lastDownloadedHeight) append(
"${innerComma()}lastDownloadedHeight=${new.processorInfo.lastDownloadedHeight}"
)
if (old.processorInfo.lastDownloadRange != new.processorInfo.lastDownloadRange) append(
"${innerComma()}lastDownloadRange=${new.processorInfo.lastDownloadRange}"
)
if (old.processorInfo.lastScanRange != new.processorInfo.lastScanRange) append(
"${innerComma()}lastScanRange=${new.processorInfo.lastScanRange}"
)
append(")")
}
if (old.saplingBalance?.available != new.saplingBalance?.available) append("${maybeComma()}availableBalance=${new.saplingBalance?.available}")
@ -371,7 +406,12 @@ class HomeFragment : BaseFragment<FragmentHomeBinding>() {
onNoFunds()
} else {
setBanner("")
setAvailable(uiModel.saplingBalance?.available, uiModel.saplingBalance?.total, uiModel.transparentBalance?.available, uiModel.unminedCount)
setAvailable(
uiModel.saplingBalance?.available,
uiModel.saplingBalance?.total,
uiModel.transparentBalance?.available,
uiModel.unminedCount
)
}
autoShield(uiModel)
}
@ -379,24 +419,43 @@ class HomeFragment : BaseFragment<FragmentHomeBinding>() {
private fun autoShield(uiModel: HomeViewModel.UiModel) {
// TODO: Move the preference read to a suspending function
// First time SharedPreferences are hit, it'll perform disk IO
val isAutoshieldingAcknowledged = Preferences.isAcknowledgedAutoshieldingInformationPrompt.get(requireApplicationContext())
val isAutoshieldingAcknowledged =
Preferences.isAcknowledgedAutoshieldingInformationPrompt.get(requireApplicationContext())
val canAutoshield = AutoShieldFragment.canAutoshield(requireApplicationContext())
if (uiModel.hasAutoshieldFunds && canAutoshield) {
if (!isAutoshieldingAcknowledged) {
mainActivity?.safeNavigate(HomeFragmentDirections.actionNavHomeToAutoshieldingInfo(true))
mainActivity?.safeNavigate(
HomeFragmentDirections.actionNavHomeToAutoshieldingInfo(
true
)
)
} else {
twig("Autoshielding is available! Let's do this!!!")
mainActivity?.safeNavigate(HomeFragmentDirections.actionNavHomeToNavFundsAvailable())
}
} else {
if (!isAutoshieldingAcknowledged) {
mainActivity?.safeNavigate(HomeFragmentDirections.actionNavHomeToAutoshieldingInfo(false))
mainActivity?.safeNavigate(
HomeFragmentDirections.actionNavHomeToAutoshieldingInfo(
false
)
)
}
// troubleshooting logs
if ((uiModel.transparentBalance?.available?.value ?: 0) > 0) {
twig("Transparent funds are available but not enough to autoshield. Available: ${uiModel.transparentBalance?.available.convertZatoshiToZecString(10)} Required: ${Zatoshi(ZcashWalletApp.instance.autoshieldThreshold).convertZatoshiToZecString(8)}")
twig(
"Transparent funds are available but not enough to autoshield. Available: ${
uiModel.transparentBalance?.available.convertZatoshiToZecString(
10
)
} Required: ${
Zatoshi(ZcashWalletApp.instance.autoshieldThreshold).convertZatoshiToZecString(
8
)
}"
)
} else if ((uiModel.transparentBalance?.total?.value ?: 0) > 0) {
twig("Transparent funds have been received but they require 10 confirmations for autoshielding.")
} else if (!canAutoshield) {
@ -506,6 +565,7 @@ class HomeFragment : BaseFragment<FragmentHomeBinding>() {
// - we want occasional random feedback that does not occur too often
return !hasInterrupted && Math.random() < 0.01
}
/**
* Interrupt the user with the various things that we want to interrupt them with. These
* requirements are driven by the product manager and may change over time.
@ -531,6 +591,7 @@ class HomeFragment : BaseFragment<FragmentHomeBinding>() {
onFeedbackProvided(ratings.indexOfFirst { it.isActivated })
}
}
fun onAskLaterClicked(view: View) {
dialog.dismiss()
}
@ -560,7 +621,15 @@ class HomeFragment : BaseFragment<FragmentHomeBinding>() {
}
.setPositiveButton("Nope") { dialog, which ->
Toast.makeText(mainActivity, R.string.feedback_thanks, Toast.LENGTH_LONG).show()
mainActivity?.reportFunnel(Report.Funnel.UserFeedback.Submitted(rating, "truncated", "truncated", "truncated", true))
mainActivity?.reportFunnel(
Report.Funnel.UserFeedback.Submitted(
rating,
"truncated",
"truncated",
"truncated",
true
)
)
dialog.dismiss()
}.show()
}
@ -568,25 +637,32 @@ class HomeFragment : BaseFragment<FragmentHomeBinding>() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
}
override fun onStart() {
super.onStart()
twig("HomeFragment.onStart")
}
override fun onPause() {
super.onPause()
}
override fun onStop() {
super.onStop()
}
override fun onDestroyView() {
super.onDestroyView()
}
override fun onDestroy() {
super.onDestroy()
}
override fun onDetach() {
super.onDetach()
}

View File

@ -3,13 +3,10 @@ package cash.z.ecc.android.ui.home
import androidx.lifecycle.ViewModel
import cash.z.ecc.android.R
import cash.z.ecc.android.ZcashWalletApp
import cash.z.ecc.android.di.DependenciesHolder
import cash.z.ecc.android.ext.toAppString
import cash.z.ecc.android.sdk.Synchronizer
import cash.z.ecc.android.sdk.Synchronizer.Status.DISCONNECTED
import cash.z.ecc.android.sdk.Synchronizer.Status.DOWNLOADING
import cash.z.ecc.android.sdk.Synchronizer.Status.SCANNING
import cash.z.ecc.android.sdk.Synchronizer.Status.SYNCED
import cash.z.ecc.android.sdk.Synchronizer.Status.VALIDATING
import cash.z.ecc.android.sdk.Synchronizer.Status.*
import cash.z.ecc.android.sdk.block.CompactBlockProcessor
import cash.z.ecc.android.sdk.db.entity.PendingTransaction
import cash.z.ecc.android.sdk.db.entity.isMined
@ -19,22 +16,12 @@ import cash.z.ecc.android.sdk.model.WalletBalance
import cash.z.ecc.android.sdk.model.Zatoshi
import cash.z.ecc.android.util.twig
import kotlinx.coroutines.channels.ConflatedBroadcastChannel
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.asFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.conflate
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.flow.scan
import javax.inject.Inject
import kotlinx.coroutines.flow.*
import kotlin.math.roundToInt
// There are deprecations with the use of BroadcastChannel
@kotlinx.coroutines.ObsoleteCoroutinesApi
class HomeViewModel @Inject constructor() : ViewModel() {
@Inject
lateinit var synchronizer: Synchronizer
class HomeViewModel : ViewModel() {
lateinit var uiModels: Flow<UiModel>
@ -82,7 +69,7 @@ class HomeViewModel @Inject constructor() : ViewModel() {
}
}
twig("initializing view models stream")
uiModels = synchronizer.run {
uiModels = DependenciesHolder.synchronizer.run {
combine(
status,
processorInfo,

View File

@ -7,10 +7,10 @@ import android.text.Spanned
import android.view.LayoutInflater
import android.view.View
import android.widget.Toast
import androidx.fragment.app.viewModels
import androidx.lifecycle.lifecycleScope
import cash.z.ecc.android.R
import cash.z.ecc.android.databinding.FragmentAwesomeBinding
import cash.z.ecc.android.di.viewmodel.viewModel
import cash.z.ecc.android.ext.distribute
import cash.z.ecc.android.ext.invisibleIf
import cash.z.ecc.android.ext.onClickNavBack
@ -31,7 +31,7 @@ import kotlinx.coroutines.launch
class AwesomeFragment : BaseFragment<FragmentAwesomeBinding>() {
override val screen = Report.Screen.AWESOME
private val viewModel: ProfileViewModel by viewModel()
private val viewModel: ProfileViewModel by viewModels()
private var lastBalance: WalletBalance? = null

View File

@ -7,29 +7,17 @@ import android.view.LayoutInflater
import android.view.View
import android.widget.Toast
import androidx.core.content.FileProvider.getUriForFile
import androidx.fragment.app.viewModels
import androidx.lifecycle.viewModelScope
import cash.z.ecc.android.BuildConfig
import cash.z.ecc.android.R
import cash.z.ecc.android.ZcashWalletApp
import cash.z.ecc.android.databinding.FragmentProfileBinding
import cash.z.ecc.android.di.viewmodel.viewModel
import cash.z.ecc.android.ext.find
import cash.z.ecc.android.ext.onClick
import cash.z.ecc.android.ext.onClickNavBack
import cash.z.ecc.android.ext.onClickNavTo
import cash.z.ecc.android.ext.showConfirmation
import cash.z.ecc.android.ext.showCriticalMessage
import cash.z.ecc.android.ext.showRescanWalletDialog
import cash.z.ecc.android.ext.*
import cash.z.ecc.android.feedback.FeedbackFile
import cash.z.ecc.android.feedback.Report
import cash.z.ecc.android.feedback.Report.Funnel.UserFeedback
import cash.z.ecc.android.feedback.Report.Tap.AWESOME_OPEN
import cash.z.ecc.android.feedback.Report.Tap.PROFILE_BACKUP
import cash.z.ecc.android.feedback.Report.Tap.PROFILE_CLOSE
import cash.z.ecc.android.feedback.Report.Tap.PROFILE_RESCAN
import cash.z.ecc.android.feedback.Report.Tap.PROFILE_SEND_FEEDBACK
import cash.z.ecc.android.feedback.Report.Tap.PROFILE_VIEW_DEV_LOGS
import cash.z.ecc.android.feedback.Report.Tap.PROFILE_VIEW_USER_LOGS
import cash.z.ecc.android.feedback.Report.Tap.*
import cash.z.ecc.android.sdk.SdkSynchronizer
import cash.z.ecc.android.sdk.ext.toAbbreviatedAddress
import cash.z.ecc.android.ui.MainActivity
@ -43,7 +31,7 @@ import java.io.File
class ProfileFragment : BaseFragment<FragmentProfileBinding>() {
override val screen = Report.Screen.PROFILE
private val viewModel: ProfileViewModel by viewModel()
private val viewModel: ProfileViewModel by viewModels()
override fun inflate(inflater: LayoutInflater): FragmentProfileBinding =
FragmentProfileBinding.inflate(inflater)

View File

@ -3,6 +3,7 @@ package cash.z.ecc.android.ui.profile
import android.widget.Toast
import androidx.lifecycle.ViewModel
import cash.z.ecc.android.ZcashWalletApp
import cash.z.ecc.android.di.DependenciesHolder
import cash.z.ecc.android.ext.Const
import cash.z.ecc.android.lockbox.LockBox
import cash.z.ecc.android.sdk.Initializer
@ -16,22 +17,16 @@ import cash.z.ecc.android.util.twig
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.runBlocking
import javax.inject.Inject
import javax.inject.Named
import kotlin.time.DurationUnit
import kotlin.time.toDuration
class ProfileViewModel @Inject constructor() : ViewModel() {
class ProfileViewModel : ViewModel() {
@Inject
lateinit var synchronizer: Synchronizer
val synchronizer: Synchronizer = DependenciesHolder.synchronizer
@Inject
lateinit var lockBox: LockBox
private val lockBox: LockBox = DependenciesHolder.lockBox
@Inject
@Named(Const.Name.APP_PREFS)
lateinit var prefs: LockBox
private val prefs: LockBox = DependenciesHolder.prefs
// TODO: track this in the app and then fetch. For now, just estimate the blocks per second.
val bps = 40
@ -156,10 +151,12 @@ class ProfileViewModel @Inject constructor() : ViewModel() {
val duration = (blocks.value / bps.toDouble()).toDuration(DurationUnit.SECONDS)
return duration.toString(DurationUnit.MINUTES).replace("m", " minutes")
}
fun blocksToMinutesString(blocks: Int): String {
val duration = (blocks / bps.toDouble()).toDuration(DurationUnit.SECONDS)
return duration.toString(DurationUnit.MINUTES).replace("m", " minutes")
}
fun blocksToMinutesString(blocks: Long): String {
val duration = (blocks / bps.toDouble()).toDuration(DurationUnit.SECONDS)
return duration.toString(DurationUnit.MINUTES).replace("m", " minutes")

View File

@ -7,10 +7,10 @@ import android.text.Spanned
import android.view.LayoutInflater
import android.view.View
import android.widget.TextView
import androidx.fragment.app.viewModels
import cash.z.android.qrecycler.QRecycler
import cash.z.ecc.android.BuildConfig
import cash.z.ecc.android.databinding.FragmentTabReceiveShieldedBinding
import cash.z.ecc.android.di.viewmodel.viewModel
import cash.z.ecc.android.ext.distribute
import cash.z.ecc.android.feedback.Report
import cash.z.ecc.android.ui.base.BaseFragment
@ -22,7 +22,7 @@ class ReceiveTabFragment :
BaseFragment<FragmentTabReceiveShieldedBinding>() {
override val screen = Report.Screen.RECEIVE
private val viewModel: ReceiveViewModel by viewModel()
private val viewModel: ReceiveViewModel by viewModels()
lateinit var qrecycler: QRecycler

View File

@ -1,14 +1,13 @@
package cash.z.ecc.android.ui.receive
import androidx.lifecycle.ViewModel
import cash.z.ecc.android.di.DependenciesHolder
import cash.z.ecc.android.sdk.Synchronizer
import cash.z.ecc.android.util.twig
import javax.inject.Inject
class ReceiveViewModel @Inject constructor() : ViewModel() {
class ReceiveViewModel : ViewModel() {
@Inject
lateinit var synchronizer: Synchronizer
private val synchronizer: Synchronizer = DependenciesHolder.synchronizer
suspend fun getAddress(): String = synchronizer.getAddress()
suspend fun getTranparentAddress(): String = synchronizer.getTransparentAddress()

View File

@ -7,10 +7,10 @@ import android.text.Spanned
import android.view.LayoutInflater
import android.view.View
import android.widget.TextView
import androidx.fragment.app.viewModels
import cash.z.android.qrecycler.QRecycler
import cash.z.ecc.android.BuildConfig
import cash.z.ecc.android.databinding.FragmentTabReceiveTransparentBinding
import cash.z.ecc.android.di.viewmodel.viewModel
import cash.z.ecc.android.ext.distribute
import cash.z.ecc.android.feedback.Report
import cash.z.ecc.android.ui.base.BaseFragment
@ -22,7 +22,7 @@ class TransparentTabFragment :
BaseFragment<FragmentTabReceiveTransparentBinding>() {
override val screen = Report.Screen.RECEIVE
private val viewModel: ReceiveViewModel by viewModel()
private val viewModel: ReceiveViewModel by viewModels()
lateinit var qrecycler: QRecycler

View File

@ -6,17 +6,13 @@ import android.os.Bundle
import android.util.DisplayMetrics
import android.view.LayoutInflater
import android.view.View
import androidx.camera.core.AspectRatio
import androidx.camera.core.CameraSelector
import androidx.camera.core.ImageAnalysis
import androidx.camera.core.ImageProxy
import androidx.camera.core.Preview
import androidx.camera.core.*
import androidx.camera.lifecycle.ProcessCameraProvider
import androidx.core.content.ContextCompat
import androidx.fragment.app.activityViewModels
import androidx.fragment.app.viewModels
import cash.z.ecc.android.R
import cash.z.ecc.android.databinding.FragmentScanBinding
import cash.z.ecc.android.di.viewmodel.activityViewModel
import cash.z.ecc.android.di.viewmodel.viewModel
import cash.z.ecc.android.ext.onClickNavBack
import cash.z.ecc.android.feedback.Report
import cash.z.ecc.android.feedback.Report.Tap.SCAN_BACK
@ -32,9 +28,9 @@ class ScanFragment : BaseFragment<FragmentScanBinding>() {
override val screen = Report.Screen.SCAN
private val viewModel: ScanViewModel by viewModel()
private val viewModel: ScanViewModel by viewModels()
private val sendViewModel: SendViewModel by activityViewModel()
private val sendViewModel: SendViewModel by activityViewModels()
private lateinit var cameraProviderFuture: ListenableFuture<ProcessCameraProvider>

View File

@ -1,14 +1,13 @@
package cash.z.ecc.android.ui.scan
import androidx.lifecycle.ViewModel
import cash.z.ecc.android.di.DependenciesHolder
import cash.z.ecc.android.sdk.Synchronizer
import cash.z.ecc.android.util.twig
import javax.inject.Inject
class ScanViewModel @Inject constructor() : ViewModel() {
class ScanViewModel : ViewModel() {
@Inject
lateinit var synchronizer: Synchronizer
private val synchronizer: Synchronizer = DependenciesHolder.synchronizer
val networkName get() = synchronizer.network.networkName

View File

@ -7,10 +7,10 @@ import android.view.LayoutInflater
import android.view.View
import android.widget.Toast
import androidx.core.view.isVisible
import androidx.fragment.app.viewModels
import androidx.lifecycle.lifecycleScope
import cash.z.ecc.android.R
import cash.z.ecc.android.databinding.FragmentAutoShieldBinding
import cash.z.ecc.android.di.viewmodel.viewModel
import cash.z.ecc.android.ext.goneIf
import cash.z.ecc.android.ext.invisibleIf
import cash.z.ecc.android.ext.requireApplicationContext
@ -18,13 +18,7 @@ import cash.z.ecc.android.feedback.Report
import cash.z.ecc.android.preference.Preferences
import cash.z.ecc.android.preference.model.get
import cash.z.ecc.android.preference.model.put
import cash.z.ecc.android.sdk.db.entity.PendingTransaction
import cash.z.ecc.android.sdk.db.entity.isCancelled
import cash.z.ecc.android.sdk.db.entity.isCreated
import cash.z.ecc.android.sdk.db.entity.isCreating
import cash.z.ecc.android.sdk.db.entity.isFailedEncoding
import cash.z.ecc.android.sdk.db.entity.isFailure
import cash.z.ecc.android.sdk.db.entity.isSubmitSuccess
import cash.z.ecc.android.sdk.db.entity.*
import cash.z.ecc.android.sdk.ext.collectWith
import cash.z.ecc.android.ui.base.BaseFragment
import cash.z.ecc.android.util.twig
@ -36,7 +30,7 @@ import java.time.Clock
class AutoShieldFragment : BaseFragment<FragmentAutoShieldBinding>() {
override val screen = Report.Screen.AUTO_SHIELD_FINAL
private val viewModel: AutoShieldViewModel by viewModel()
private val viewModel: AutoShieldViewModel by viewModels()
private val uiModels = MutableStateFlow(UiModel())

View File

@ -2,6 +2,7 @@ package cash.z.ecc.android.ui.send
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import cash.z.ecc.android.di.DependenciesHolder
import cash.z.ecc.android.ext.Const
import cash.z.ecc.android.lockbox.LockBox
import cash.z.ecc.android.sdk.Synchronizer
@ -20,39 +21,42 @@ import kotlinx.coroutines.flow.combineTransform
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import javax.inject.Inject
class AutoShieldViewModel @Inject constructor() : ViewModel() {
class AutoShieldViewModel : ViewModel() {
@Inject
lateinit var synchronizer: Synchronizer
private val synchronizer: Synchronizer = DependenciesHolder.synchronizer
@Inject
lateinit var lockBox: LockBox
private val lockBox: LockBox = DependenciesHolder.lockBox
var latestBalance: BalanceModel? = null
val balances get() = combineTransform(
synchronizer.orchardBalances,
synchronizer.saplingBalances,
synchronizer.transparentBalances,
) { o, s, t ->
BalanceModel(o, s, t).let {
latestBalance = it
emit(it)
val balances
get() = combineTransform(
synchronizer.orchardBalances,
synchronizer.saplingBalances,
synchronizer.transparentBalances,
) { o, s, t ->
BalanceModel(o, s, t).let {
latestBalance = it
emit(it)
}
}
}
val statuses get() = combineTransform(synchronizer.saplingBalances, synchronizer.pendingTransactions, synchronizer.processorInfo) { balance, pending, info ->
val unconfirmed = pending.filter { !it.isConfirmed(info.networkBlockHeight) }
val unmined = pending.filter { it.isSubmitSuccess() && !it.isMined() }
val pending = balance?.pending?.value ?: 0
emit(StatusModel(unmined, unconfirmed, pending, info.networkBlockHeight))
}
val statuses
get() = combineTransform(
synchronizer.saplingBalances,
synchronizer.pendingTransactions,
synchronizer.processorInfo
) { balance, pending, info ->
val unconfirmed = pending.filter { !it.isConfirmed(info.networkBlockHeight) }
val unmined = pending.filter { it.isSubmitSuccess() && !it.isMined() }
val pending = balance?.pending?.value ?: 0
emit(StatusModel(unmined, unconfirmed, pending, info.networkBlockHeight))
}
private fun PendingTransaction.isConfirmed(networkBlockHeight: BlockHeight?): Boolean {
return networkBlockHeight?.let { height ->
isMined() && (height.value - minedHeight + 1) > 10
isMined() && (height.value - minedHeight + 1) > 10
} ?: false
}
@ -90,7 +94,11 @@ class AutoShieldViewModel @Inject constructor() : ViewModel() {
synchronizer.network
)
}
synchronizer.shieldFunds(sk, tsk, "${ZcashSdk.DEFAULT_SHIELD_FUNDS_MEMO_PREFIX}\nAll UTXOs from $addr").onEach { tx ->
synchronizer.shieldFunds(
sk,
tsk,
"${ZcashSdk.DEFAULT_SHIELD_FUNDS_MEMO_PREFIX}\nAll UTXOs from $addr"
).onEach { tx ->
twig("Received shielding txUpdate: ${tx?.toString()}")
// updateMetrics(it)
// reportFailures(it)
@ -105,10 +113,13 @@ class AutoShieldViewModel @Inject constructor() : ViewModel() {
) {
val balanceShielded: String = saplingBalance?.available.toDisplay()
val balanceTransparent: String = transparentBalance?.available.toDisplay()
val balanceTotal: String = ((saplingBalance?.available ?: Zatoshi(0)) + (transparentBalance?.available ?: Zatoshi(0))).toDisplay()
val balanceTotal: String =
((saplingBalance?.available ?: Zatoshi(0)) + (transparentBalance?.available
?: Zatoshi(0))).toDisplay()
val canAutoShield: Boolean = (transparentBalance?.available?.value ?: 0) > 0L
val maxLength = maxOf(balanceShielded.length, balanceTransparent.length, balanceTotal.length)
val maxLength =
maxOf(balanceShielded.length, balanceTransparent.length, balanceTotal.length)
val paddedShielded = pad(balanceShielded)
val paddedTransparent = pad(balanceTransparent)
val paddedTotal = pad(balanceTotal)

View File

@ -5,20 +5,15 @@ import android.content.Context
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import androidx.fragment.app.activityViewModels
import cash.z.ecc.android.R
import cash.z.ecc.android.databinding.FragmentSendFinalBinding
import cash.z.ecc.android.di.viewmodel.activityViewModel
import cash.z.ecc.android.ext.WalletZecFormmatter
import cash.z.ecc.android.ext.goneIf
import cash.z.ecc.android.feedback.Report
import cash.z.ecc.android.feedback.Report.Tap.SEND_FINAL_CLOSE
import cash.z.ecc.android.sdk.SdkSynchronizer
import cash.z.ecc.android.sdk.db.entity.PendingTransaction
import cash.z.ecc.android.sdk.db.entity.isCancelled
import cash.z.ecc.android.sdk.db.entity.isCreating
import cash.z.ecc.android.sdk.db.entity.isFailedEncoding
import cash.z.ecc.android.sdk.db.entity.isFailure
import cash.z.ecc.android.sdk.db.entity.isSubmitSuccess
import cash.z.ecc.android.sdk.db.entity.*
import cash.z.ecc.android.sdk.ext.toAbbreviatedAddress
import cash.z.ecc.android.sdk.model.Zatoshi
import cash.z.ecc.android.ui.base.BaseFragment
@ -29,7 +24,7 @@ import kotlinx.coroutines.flow.onEach
class SendFinalFragment : BaseFragment<FragmentSendFinalBinding>() {
override val screen = Report.Screen.SEND_FINAL
private val sendViewModel: SendViewModel by activityViewModel()
private val sendViewModel: SendViewModel by activityViewModels()
override fun inflate(inflater: LayoutInflater): FragmentSendFinalBinding =
FragmentSendFinalBinding.inflate(inflater)

View File

@ -14,11 +14,11 @@ import androidx.core.view.isGone
import androidx.core.view.isVisible
import androidx.core.widget.ImageViewCompat
import androidx.core.widget.doAfterTextChanged
import androidx.fragment.app.activityViewModels
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.viewModelScope
import cash.z.ecc.android.R
import cash.z.ecc.android.databinding.FragmentSendBinding
import cash.z.ecc.android.di.viewmodel.activityViewModel
import cash.z.ecc.android.ext.*
import cash.z.ecc.android.feedback.Report
import cash.z.ecc.android.feedback.Report.Tap.*
@ -42,7 +42,7 @@ class SendFragment :
private var maxZatoshi: Long? = null
private var availableZatoshi: Long? = null
val sendViewModel: SendViewModel by activityViewModel()
val sendViewModel: SendViewModel by activityViewModels()
override fun inflate(inflater: LayoutInflater): FragmentSendBinding =
FragmentSendBinding.inflate(inflater)
@ -123,8 +123,10 @@ class SendFragment :
private fun onMemoUpdated() {
val totalLength = sendViewModel.createMemoToSend().length
binding.textLayoutMemo.helperText = "$totalLength/${ZcashSdk.MAX_MEMO_SIZE} ${getString(R.string.send_memo_chars_abbreviation)}"
val color = if (totalLength > ZcashSdk.MAX_MEMO_SIZE) R.color.zcashRed else R.color.text_light_dimmed
binding.textLayoutMemo.helperText =
"$totalLength/${ZcashSdk.MAX_MEMO_SIZE} ${getString(R.string.send_memo_chars_abbreviation)}"
val color =
if (totalLength > ZcashSdk.MAX_MEMO_SIZE) R.color.zcashRed else R.color.text_light_dimmed
binding.textLayoutMemo.setHelperTextColor(ColorStateList.valueOf(color.toAppColor()))
}
@ -191,21 +193,28 @@ class SendFragment :
private fun onSubmit(unused: EditText? = null) {
sendViewModel.toAddress = binding.inputZcashAddress.text.toString()
sendViewModel.validate(requireContext(), availableZatoshi, maxZatoshi).onFirstWith(resumedScope) { errorMessage ->
if (errorMessage == null) {
val symbol = getString(R.string.symbol)
mainActivity?.authenticate("${getString(R.string.send_confirmation_prompt)}\n${WalletZecFormmatter.toZecStringFull(sendViewModel.zatoshiAmount)} $symbol ${getString(R.string.send_final_to)}\n${sendViewModel.toAddress.toAbbreviatedAddress()}") {
sendViewModel.validate(requireContext(), availableZatoshi, maxZatoshi)
.onFirstWith(resumedScope) { errorMessage ->
if (errorMessage == null) {
val symbol = getString(R.string.symbol)
mainActivity?.authenticate(
"${getString(R.string.send_confirmation_prompt)}\n${
WalletZecFormmatter.toZecStringFull(
sendViewModel.zatoshiAmount
)
} $symbol ${getString(R.string.send_final_to)}\n${sendViewModel.toAddress.toAbbreviatedAddress()}"
) {
// sendViewModel.funnel(Send.AddressPageComplete)
mainActivity?.safeNavigate(R.id.action_nav_send_to_nav_send_final)
}
} else {
resumedScope.launch {
binding.textAddressError.text = errorMessage
delay(2500L)
binding.textAddressError.text = ""
mainActivity?.safeNavigate(R.id.action_nav_send_to_nav_send_final)
}
} else {
resumedScope.launch {
binding.textAddressError.text = errorMessage
delay(2500L)
binding.textAddressError.text = ""
}
}
}
}
}
private fun onMax() {
@ -306,7 +315,10 @@ class SendFragment :
group.visible()
addressTextView.text = address.toAbbreviatedAddress(16, 16)
checkIcon.goneIf(!selected)
ImageViewCompat.setImageTintList(shieldIcon, ColorStateList.valueOf(if (selected) R.color.colorPrimary.toAppColor() else R.color.zcashWhite_12.toAppColor()))
ImageViewCompat.setImageTintList(
shieldIcon,
ColorStateList.valueOf(if (selected) R.color.colorPrimary.toAppColor() else R.color.zcashWhite_12.toAppColor())
)
addressLabel.setText(if (address == userShieldedAddr) R.string.send_banner_address_user else R.string.send_banner_address_unknown)
if (address == userTransparentAddr) addressLabel.setText("Your Auto-Shielding Address")
addressLabel.setTextColor(if (selected) R.color.colorPrimary.toAppColor() else R.color.text_light.toAppColor())
@ -349,7 +361,8 @@ class SendFragment :
private var lastUsedAddress: String? = null
private suspend fun loadLastUsedAddress(): String? {
if (lastUsedAddress == null) {
lastUsedAddress = sendViewModel.synchronizer.sentTransactions.first().firstOrNull { !it.toAddress.isNullOrEmpty() }?.toAddress
lastUsedAddress = sendViewModel.synchronizer.sentTransactions.first()
.firstOrNull { !it.toAddress.isNullOrEmpty() }?.toAddress
updateLastUsedBanner(lastUsedAddress, binding.imageLastUsedAddressSelected.isVisible)
}
return lastUsedAddress

View File

@ -4,26 +4,22 @@ import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import androidx.core.widget.doAfterTextChanged
import androidx.fragment.app.activityViewModels
import cash.z.ecc.android.R
import cash.z.ecc.android.databinding.FragmentSendMemoBinding
import cash.z.ecc.android.di.viewmodel.activityViewModel
import cash.z.ecc.android.ext.gone
import cash.z.ecc.android.ext.goneIf
import cash.z.ecc.android.ext.onEditorActionDone
import cash.z.ecc.android.feedback.Report
import cash.z.ecc.android.feedback.Report.Funnel.Send
import cash.z.ecc.android.feedback.Report.Tap.SEND_MEMO_CLEAR
import cash.z.ecc.android.feedback.Report.Tap.SEND_MEMO_EXCLUDE
import cash.z.ecc.android.feedback.Report.Tap.SEND_MEMO_INCLUDE
import cash.z.ecc.android.feedback.Report.Tap.SEND_MEMO_NEXT
import cash.z.ecc.android.feedback.Report.Tap.SEND_MEMO_SKIP
import cash.z.ecc.android.feedback.Report.Tap.*
import cash.z.ecc.android.ui.base.BaseFragment
import cash.z.ecc.android.ui.util.INCLUDE_MEMO_PREFIX_STANDARD
class SendMemoFragment : BaseFragment<FragmentSendMemoBinding>() {
override val screen = Report.Screen.SEND_MEMO
val sendViewModel: SendViewModel by activityViewModel()
val sendViewModel: SendViewModel by activityViewModels()
override fun inflate(inflater: LayoutInflater): FragmentSendMemoBinding =
FragmentSendMemoBinding.inflate(inflater)
@ -59,7 +55,8 @@ class SendMemoFragment : BaseFragment<FragmentSendMemoBinding>() {
}
sendViewModel.afterInitFromAddress {
binding.textIncludedAddress.text = "$INCLUDE_MEMO_PREFIX_STANDARD ${sendViewModel.fromAddress}"
binding.textIncludedAddress.text =
"$INCLUDE_MEMO_PREFIX_STANDARD ${sendViewModel.fromAddress}"
}
binding.textIncludedAddress.gone()

View File

@ -5,6 +5,7 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import cash.z.ecc.android.R
import cash.z.ecc.android.ZcashWalletApp
import cash.z.ecc.android.di.DependenciesHolder
import cash.z.ecc.android.ext.Const
import cash.z.ecc.android.ext.WalletZecFormmatter
import cash.z.ecc.android.feedback.Feedback
@ -16,20 +17,10 @@ import cash.z.ecc.android.feedback.Report.Funnel.Send.SendSelected
import cash.z.ecc.android.feedback.Report.Funnel.Send.SpendingKeyFound
import cash.z.ecc.android.feedback.Report.Issue
import cash.z.ecc.android.feedback.Report.MetricType
import cash.z.ecc.android.feedback.Report.MetricType.TRANSACTION_CREATED
import cash.z.ecc.android.feedback.Report.MetricType.TRANSACTION_INITIALIZED
import cash.z.ecc.android.feedback.Report.MetricType.TRANSACTION_MINED
import cash.z.ecc.android.feedback.Report.MetricType.TRANSACTION_SUBMITTED
import cash.z.ecc.android.feedback.Report.MetricType.*
import cash.z.ecc.android.lockbox.LockBox
import cash.z.ecc.android.sdk.Synchronizer
import cash.z.ecc.android.sdk.db.entity.PendingTransaction
import cash.z.ecc.android.sdk.db.entity.isCancelled
import cash.z.ecc.android.sdk.db.entity.isCreated
import cash.z.ecc.android.sdk.db.entity.isCreating
import cash.z.ecc.android.sdk.db.entity.isFailedEncoding
import cash.z.ecc.android.sdk.db.entity.isFailedSubmit
import cash.z.ecc.android.sdk.db.entity.isMined
import cash.z.ecc.android.sdk.db.entity.isSubmitSuccess
import cash.z.ecc.android.sdk.db.entity.*
import cash.z.ecc.android.sdk.ext.ZcashSdk
import cash.z.ecc.android.sdk.model.Zatoshi
import cash.z.ecc.android.sdk.tool.DerivationTool
@ -43,22 +34,17 @@ import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import java.lang.IllegalArgumentException
import javax.inject.Inject
class SendViewModel @Inject constructor() : ViewModel() {
class SendViewModel : ViewModel() {
// note used in testing
val metrics = mutableMapOf<String, TimeMetric>()
@Inject
lateinit var lockBox: LockBox
private val lockBox: LockBox = DependenciesHolder.lockBox
@Inject
lateinit var synchronizer: Synchronizer
val synchronizer: Synchronizer = DependenciesHolder.synchronizer
@Inject
lateinit var feedback: Feedback
private val feedback: Feedback = DependenciesHolder.feedback
var fromAddress: String = ""
var toAddress: String = ""
@ -68,7 +54,7 @@ class SendViewModel @Inject constructor() : ViewModel() {
set(value) {
require(!value || (value && !fromAddress.isNullOrEmpty())) {
"Error: fromAddress was empty while attempting to include it in the memo. Verify" +
" that initFromAddress() has previously been called on this viewmodel."
" that initFromAddress() has previously been called on this viewmodel."
}
field = value
}
@ -103,7 +89,8 @@ class SendViewModel @Inject constructor() : ViewModel() {
}
}
fun createMemoToSend() = if (includeFromAddress) "$memo\n$INCLUDE_MEMO_PREFIX_STANDARD\n$fromAddress" else memo
fun createMemoToSend() =
if (includeFromAddress) "$memo\n$INCLUDE_MEMO_PREFIX_STANDARD\n$fromAddress" else memo
suspend fun validateAddress(address: String): AddressType =
synchronizer.validateAddress(address)
@ -132,11 +119,21 @@ class SendViewModel @Inject constructor() : ViewModel() {
emit(context.getString(R.string.send_validation_error_dust))
}
maxZatoshi != null && zatoshiAmount?.let { it.value > maxZatoshi } ?: false -> {
emit(context.getString(R.string.send_validation_error_too_much,
WalletZecFormmatter.toZecStringFull(Zatoshi((maxZatoshi))), ZcashWalletApp.instance.getString(R.string.symbol)))
emit(
context.getString(
R.string.send_validation_error_too_much,
WalletZecFormmatter.toZecStringFull(Zatoshi((maxZatoshi))),
ZcashWalletApp.instance.getString(R.string.symbol)
)
)
}
createMemoToSend().length > ZcashSdk.MAX_MEMO_SIZE -> {
emit(context.getString(R.string.send_validation_error_memo_length, ZcashSdk.MAX_MEMO_SIZE))
emit(
context.getString(
R.string.send_validation_error_memo_length,
ZcashSdk.MAX_MEMO_SIZE
)
)
}
else -> emit(null)
}
@ -185,7 +182,8 @@ class SendViewModel @Inject constructor() : ViewModel() {
private fun reportUserInputIssues(memoToSend: String) {
if (toAddress == fromAddress) feedback.report(Issue.SelfSend)
when {
(zatoshiAmount?.value ?: 0L) < ZcashSdk.MINERS_FEE.value -> feedback.report(Issue.TinyAmount)
(zatoshiAmount?.value
?: 0L) < ZcashSdk.MINERS_FEE.value -> feedback.report(Issue.TinyAmount)
(zatoshiAmount?.value ?: 0L) < 100L -> feedback.report(Issue.MicroAmount)
(zatoshiAmount ?: 0L) == 1L -> feedback.report(Issue.MinimumAmount)
}
@ -235,7 +233,9 @@ class SendViewModel @Inject constructor() : ViewModel() {
}
// remove all top-level metrics
if (metric.key == Report.MetricType.TRANSACTION_MINED.key) metrics.remove(metricId)
if (metric.key == Report.MetricType.TRANSACTION_MINED.key) metrics.remove(
metricId
)
}
}
}
@ -247,8 +247,12 @@ class SendViewModel @Inject constructor() : ViewModel() {
feedback.report(step)
}
private operator fun MetricType.unaryPlus(): TimeMetric = TimeMetric(key, description).markTime()
private infix fun TimeMetric.by(txId: Long) = this.toMetricIdFor(txId).also { metrics[it] = this }
private operator fun MetricType.unaryPlus(): TimeMetric =
TimeMetric(key, description).markTime()
private infix fun TimeMetric.by(txId: Long) =
this.toMetricIdFor(txId).also { metrics[it] = this }
private infix fun Pair<MetricType, MetricType>.by(txId: Long): String? {
val startMetric = first.toMetricIdFor(txId).let { metricId ->
metrics[metricId].also { if (it == null) println("Warning no start metric for id: $metricId") }

View File

@ -6,17 +6,11 @@ import android.view.LayoutInflater
import android.view.View
import android.widget.Toast
import androidx.core.widget.doAfterTextChanged
import androidx.fragment.app.viewModels
import cash.z.ecc.android.R
import cash.z.ecc.android.ZcashWalletApp
import cash.z.ecc.android.databinding.FragmentSettingsBinding
import cash.z.ecc.android.di.viewmodel.viewModel
import cash.z.ecc.android.ext.gone
import cash.z.ecc.android.ext.onClickNavBack
import cash.z.ecc.android.ext.showUpdateServerCriticalError
import cash.z.ecc.android.ext.showUpdateServerDialog
import cash.z.ecc.android.ext.toAppColor
import cash.z.ecc.android.ext.toAppString
import cash.z.ecc.android.ext.visible
import cash.z.ecc.android.ext.*
import cash.z.ecc.android.sdk.exception.LightWalletException
import cash.z.ecc.android.sdk.ext.collectWith
import cash.z.ecc.android.ui.base.BaseFragment
@ -25,7 +19,7 @@ import kotlinx.coroutines.launch
class SettingsFragment : BaseFragment<FragmentSettingsBinding>() {
private val viewModel: SettingsViewModel by viewModel()
private val viewModel: SettingsViewModel by viewModels()
override fun inflate(inflater: LayoutInflater): FragmentSettingsBinding =
FragmentSettingsBinding.inflate(inflater)

View File

@ -1,24 +1,20 @@
package cash.z.ecc.android.ui.settings
import androidx.lifecycle.ViewModel
import cash.z.ecc.android.di.DependenciesHolder
import cash.z.ecc.android.ext.Const
import cash.z.ecc.android.lockbox.LockBox
import cash.z.ecc.android.sdk.Synchronizer
import cash.z.ecc.android.util.twig
import kotlinx.coroutines.flow.MutableStateFlow
import javax.inject.Inject
import javax.inject.Named
import kotlin.properties.Delegates.observable
import kotlin.reflect.KProperty
class SettingsViewModel @Inject constructor() : ViewModel() {
class SettingsViewModel : ViewModel() {
@Inject
lateinit var synchronizer: Synchronizer
private val synchronizer: Synchronizer = DependenciesHolder.synchronizer
@Inject
@Named(Const.Name.APP_PREFS)
lateinit var prefs: LockBox
private val prefs: LockBox = DependenciesHolder.prefs
lateinit var uiModels: MutableStateFlow<UiModel>

View File

@ -9,11 +9,12 @@ import android.view.View
import android.widget.TextView
import android.widget.Toast
import androidx.activity.addCallback
import androidx.fragment.app.activityViewModels
import androidx.lifecycle.lifecycleScope
import cash.z.ecc.android.R
import cash.z.ecc.android.ZcashWalletApp
import cash.z.ecc.android.databinding.FragmentBackupBinding
import cash.z.ecc.android.di.viewmodel.activityViewModel
import cash.z.ecc.android.di.DependenciesHolder
import cash.z.ecc.android.ext.Const
import cash.z.ecc.android.feedback.Report
import cash.z.ecc.android.feedback.Report.MetricType.SEED_PHRASE_LOADED
@ -21,7 +22,6 @@ import cash.z.ecc.android.feedback.Report.Tap.BACKUP_DONE
import cash.z.ecc.android.feedback.Report.Tap.BACKUP_VERIFY
import cash.z.ecc.android.feedback.measure
import cash.z.ecc.android.lockbox.LockBox
import cash.z.ecc.android.sdk.db.entity.Block
import cash.z.ecc.android.sdk.ext.ZcashSdk
import cash.z.ecc.android.sdk.model.BlockHeight
import cash.z.ecc.android.ui.base.BaseFragment
@ -35,12 +35,11 @@ import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.lang.RuntimeException
class BackupFragment : BaseFragment<FragmentBackupBinding>() {
override val screen = Report.Screen.BACKUP
private val walletSetup: WalletSetupViewModel by activityViewModel(false)
private val walletSetup: WalletSetupViewModel by activityViewModels()
private var hasBackUp: Boolean = true // TODO: implement backup and then check for it here-ish
@ -75,6 +74,7 @@ class BackupFragment : BaseFragment<FragmentBackupBinding>() {
onEnterWallet(false)
}
}
override fun onAttach(context: Context) {
super.onAttach(context)
walletSetup.checkSeed().onEach {
@ -88,7 +88,8 @@ class BackupFragment : BaseFragment<FragmentBackupBinding>() {
override fun onResume() {
super.onResume()
resumedScope.launch {
binding.textBirtdate.text = getString(R.string.backup_format_birthday_height, calculateBirthday().value)
binding.textBirtdate.text =
getString(R.string.backup_format_birthday_height, calculateBirthday().value)
}
}
@ -98,25 +99,35 @@ class BackupFragment : BaseFragment<FragmentBackupBinding>() {
var oldestTransactionHeight: BlockHeight? = null
var activationHeight: BlockHeight? = null
try {
activationHeight = mainActivity?.synchronizerComponent?.synchronizer()?.network?.saplingActivationHeight
activationHeight = DependenciesHolder.synchronizer.network.saplingActivationHeight
storedBirthday = walletSetup.loadBirthdayHeight()
oldestTransactionHeight = mainActivity?.synchronizerComponent?.synchronizer()?.receivedTransactions?.first()?.last()?.minedHeight?.let {
BlockHeight.new(ZcashWalletApp.instance.defaultNetwork, it)
}
oldestTransactionHeight = DependenciesHolder.synchronizer.receivedTransactions.first()
?.last()?.minedHeight?.let {
BlockHeight.new(ZcashWalletApp.instance.defaultNetwork, it)
}
// to be safe adjust for reorgs (and generally a little cushion is good for privacy)
// so we round down to the nearest 100 and then subtract 100 to ensure that the result is always at least 100 blocks away
oldestTransactionHeight = ZcashSdk.MAX_REORG_SIZE.let { boundary ->
oldestTransactionHeight?.let { it.value - it.value.rem(boundary) - boundary }?.let { BlockHeight.new(ZcashWalletApp.instance.defaultNetwork, it) }
oldestTransactionHeight?.let { it.value - it.value.rem(boundary) - boundary }
?.let { BlockHeight.new(ZcashWalletApp.instance.defaultNetwork, it) }
}
} catch (t: Throwable) {
twig("failed to calculate birthday due to: $t")
}
return listOfNotNull(storedBirthday, oldestTransactionHeight, activationHeight).maxBy { it.value }
return listOfNotNull(
storedBirthday,
oldestTransactionHeight,
activationHeight
).maxBy { it.value }
}
private fun onEnterWallet(showMessage: Boolean = !this.hasBackUp) {
if (showMessage) {
Toast.makeText(activity, R.string.backup_verification_not_implemented, Toast.LENGTH_LONG).show()
Toast.makeText(
activity,
R.string.backup_verification_not_implemented,
Toast.LENGTH_LONG
).show()
}
mainActivity?.navController?.popBackStack()
}
@ -138,7 +149,8 @@ class BackupFragment : BaseFragment<FragmentBackupBinding>() {
mainActivity!!.feedback.measure(SEED_PHRASE_LOADED) {
val lockBox = LockBox(ZcashWalletApp.instance)
val mnemonics = Mnemonics()
val seedPhrase = lockBox.getCharsUtf8(Const.Backup.SEED_PHRASE) ?: throw RuntimeException("Seed Phrase expected but not found in storage!!")
val seedPhrase = lockBox.getCharsUtf8(Const.Backup.SEED_PHRASE)
?: throw RuntimeException("Seed Phrase expected but not found in storage!!")
val result = mnemonics.toWordList(seedPhrase)
result
}

View File

@ -5,25 +5,17 @@ import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.widget.Toast
import androidx.fragment.app.activityViewModels
import androidx.lifecycle.lifecycleScope
import cash.z.ecc.android.R
import cash.z.ecc.android.ZcashWalletApp
import cash.z.ecc.android.databinding.FragmentLandingBinding
import cash.z.ecc.android.di.viewmodel.activityViewModel
import cash.z.ecc.android.ext.locale
import cash.z.ecc.android.ext.showSharedLibraryCriticalError
import cash.z.ecc.android.ext.toAppString
import cash.z.ecc.android.feedback.Report
import cash.z.ecc.android.feedback.Report.Funnel.Restore
import cash.z.ecc.android.feedback.Report.Tap.DEVELOPER_WALLET_CANCEL
import cash.z.ecc.android.feedback.Report.Tap.DEVELOPER_WALLET_IMPORT
import cash.z.ecc.android.feedback.Report.Tap.DEVELOPER_WALLET_PROMPT
import cash.z.ecc.android.feedback.Report.Tap.LANDING_BACKUP
import cash.z.ecc.android.feedback.Report.Tap.LANDING_BACKUP_SKIPPED_1
import cash.z.ecc.android.feedback.Report.Tap.LANDING_BACKUP_SKIPPED_2
import cash.z.ecc.android.feedback.Report.Tap.LANDING_BACKUP_SKIPPED_3
import cash.z.ecc.android.feedback.Report.Tap.LANDING_NEW
import cash.z.ecc.android.feedback.Report.Tap.LANDING_RESTORE
import cash.z.ecc.android.feedback.Report.Tap.*
import cash.z.ecc.android.sdk.model.BlockHeight
import cash.z.ecc.android.sdk.model.ZcashNetwork
import cash.z.ecc.android.ui.base.BaseFragment
@ -34,12 +26,11 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import java.lang.RuntimeException
class LandingFragment : BaseFragment<FragmentLandingBinding>() {
override val screen = Report.Screen.LANDING
private val walletSetup: WalletSetupViewModel by activityViewModel(false)
private val walletSetup: WalletSetupViewModel by activityViewModels()
private var skipCount: Int = 0
@ -50,8 +41,16 @@ class LandingFragment : BaseFragment<FragmentLandingBinding>() {
super.onViewCreated(view, savedInstanceState)
binding.buttonPositive.setOnClickListener {
when (binding.buttonPositive.text.toString().toLowerCase(locale())) {
R.string.landing_button_primary.toAppString(true) -> onNewWallet().also { tapped(LANDING_NEW) }
R.string.landing_button_primary_create_success.toAppString(true) -> onBackupWallet().also { tapped(LANDING_BACKUP) }
R.string.landing_button_primary.toAppString(true) -> onNewWallet().also {
tapped(
LANDING_NEW
)
}
R.string.landing_button_primary_create_success.toAppString(true) -> onBackupWallet().also {
tapped(
LANDING_BACKUP
)
}
}
}
binding.buttonNegative.setOnLongClickListener {
@ -86,6 +85,7 @@ class LandingFragment : BaseFragment<FragmentLandingBinding>() {
}
}
}
override fun onAttach(context: Context) {
super.onAttach(context)
@ -140,11 +140,13 @@ class LandingFragment : BaseFragment<FragmentLandingBinding>() {
// new testnet dev wallet
when (ZcashWalletApp.instance.defaultNetwork) {
ZcashNetwork.Mainnet -> {
seedPhrase = "still champion voice habit trend flight survey between bitter process artefact blind carbon truly provide dizzy crush flush breeze blouse charge solid fish spread"
seedPhrase =
"still champion voice habit trend flight survey between bitter process artefact blind carbon truly provide dizzy crush flush breeze blouse charge solid fish spread"
birthday = BlockHeight.new(ZcashNetwork.Mainnet, 991645) // 663174
}
ZcashNetwork.Testnet -> {
seedPhrase = "quantum whisper lion route fury lunar pelican image job client hundred sauce chimney barely life cliff spirit admit weekend message recipe trumpet impact kitten"
seedPhrase =
"quantum whisper lion route fury lunar pelican image job client hundred sauce chimney barely life cliff spirit admit weekend message recipe trumpet impact kitten"
birthday = BlockHeight.new(ZcashNetwork.Testnet, 1330190)
}
else -> throw RuntimeException("No developer wallet exists for network ${ZcashWalletApp.instance.defaultNetwork}")
@ -153,7 +155,8 @@ class LandingFragment : BaseFragment<FragmentLandingBinding>() {
mainActivity?.apply {
lifecycleScope.launch {
try {
mainActivity?.startSync(walletSetup.importWallet(seedPhrase, birthday))
walletSetup.importWallet(seedPhrase, birthday)
mainActivity?.startSync()
binding.buttonPositive.isEnabled = true
binding.textMessage.setText(R.string.landing_import_success_message)
binding.buttonNegative.setText(R.string.landing_button_secondary_import_success)
@ -173,13 +176,8 @@ class LandingFragment : BaseFragment<FragmentLandingBinding>() {
binding.buttonPositive.isEnabled = false
try {
val initializer = walletSetup.newWallet()
// if (!initializer.overwriteVks .accountsCreated) {
// binding.buttonPositive.isEnabled = true
// binding.buttonPositive.setText(R.string.landing_button_primary)
// throw IllegalStateException("New wallet should result in accounts table being created")
// }
mainActivity?.startSync(initializer)
walletSetup.newWallet()
mainActivity?.startSync()
binding.buttonPositive.isEnabled = true
binding.textMessage.setText(R.string.landing_create_success_message)

View File

@ -11,22 +11,19 @@ import android.view.MotionEvent.ACTION_DOWN
import android.view.MotionEvent.ACTION_UP
import android.view.View
import android.widget.TextView
import androidx.fragment.app.activityViewModels
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.RecyclerView
import cash.z.ecc.android.R
import cash.z.ecc.android.ZcashWalletApp
import cash.z.ecc.android.databinding.FragmentRestoreBinding
import cash.z.ecc.android.di.viewmodel.activityViewModel
import cash.z.ecc.android.ext.goneIf
import cash.z.ecc.android.ext.showConfirmation
import cash.z.ecc.android.ext.showInvalidSeedPhraseError
import cash.z.ecc.android.ext.showSharedLibraryCriticalError
import cash.z.ecc.android.feedback.Report
import cash.z.ecc.android.feedback.Report.Funnel.Restore
import cash.z.ecc.android.feedback.Report.Tap.RESTORE_BACK
import cash.z.ecc.android.feedback.Report.Tap.RESTORE_CLEAR
import cash.z.ecc.android.feedback.Report.Tap.RESTORE_DONE
import cash.z.ecc.android.feedback.Report.Tap.RESTORE_SUCCESS
import cash.z.ecc.android.feedback.Report.Tap.*
import cash.z.ecc.android.sdk.model.BlockHeight
import cash.z.ecc.android.ui.base.BaseFragment
import com.google.android.material.dialog.MaterialAlertDialogBuilder
@ -38,7 +35,7 @@ import kotlinx.coroutines.launch
class RestoreFragment : BaseFragment<FragmentRestoreBinding>(), View.OnKeyListener {
override val screen = Report.Screen.RESTORE
private val walletSetup: WalletSetupViewModel by activityViewModel(false)
private val walletSetup: WalletSetupViewModel by activityViewModels()
private lateinit var seedWordRecycler: RecyclerView
private var seedWordAdapter: SeedWordAdapter? = null
@ -151,7 +148,8 @@ class RestoreFragment : BaseFragment<FragmentRestoreBinding>(), View.OnKeyListen
mainActivity?.apply {
lifecycleScope.launch {
try {
mainActivity?.startSync(walletSetup.importWallet(seedPhrase, birthday))
walletSetup.importWallet(seedPhrase, birthday)
mainActivity?.startSync()
// bugfix: if the user proceeds before the synchronizer is created the app will crash!
binding.buttonSuccess.isEnabled = true
mainActivity?.reportFunnel(Restore.ImportCompleted)

View File

@ -3,6 +3,7 @@ package cash.z.ecc.android.ui.setup
import android.content.Context
import androidx.lifecycle.ViewModel
import cash.z.ecc.android.ZcashWalletApp
import cash.z.ecc.android.di.DependenciesHolder
import cash.z.ecc.android.ext.Const
import cash.z.ecc.android.ext.failWith
import cash.z.ecc.android.feedback.Feedback
@ -12,34 +13,26 @@ import cash.z.ecc.android.sdk.Initializer
import cash.z.ecc.android.sdk.exception.InitializerException
import cash.z.ecc.android.sdk.model.BlockHeight
import cash.z.ecc.android.sdk.model.LightWalletEndpoint
import cash.z.ecc.android.sdk.model.ZcashNetwork
import cash.z.ecc.android.sdk.tool.DerivationTool
import cash.z.ecc.android.sdk.type.UnifiedViewingKey
import cash.z.ecc.android.sdk.model.ZcashNetwork
import cash.z.ecc.android.ui.setup.WalletSetupViewModel.WalletSetupState.*
import cash.z.ecc.android.util.twig
import cash.z.ecc.kotlin.mnemonic.Mnemonics
import com.bugsnag.android.Bugsnag
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.withContext
import javax.inject.Inject
import javax.inject.Named
class WalletSetupViewModel @Inject constructor() : ViewModel() {
class WalletSetupViewModel : ViewModel() {
@Inject
lateinit var mnemonics: Mnemonics
private val mnemonics: Mnemonics = DependenciesHolder.mnemonics
@Inject
lateinit var lockBox: LockBox
private val lockBox: LockBox = DependenciesHolder.lockBox
@Inject
@Named(Const.Name.APP_PREFS)
lateinit var prefs: LockBox
private val prefs: LockBox = DependenciesHolder.prefs
@Inject
lateinit var feedback: Feedback
private val feedback: Feedback = DependenciesHolder.feedback
enum class WalletSetupState {
SEED_WITH_BACKUP, SEED_WITHOUT_BACKUP, NO_SEED
@ -69,25 +62,28 @@ class WalletSetupViewModel @Inject constructor() : ViewModel() {
return null
}
suspend fun newWallet(): Initializer {
suspend fun newWallet() {
val network = ZcashWalletApp.instance.defaultNetwork
twig("Initializing new ${network.networkName} wallet")
with(mnemonics) {
storeWallet(nextMnemonic(nextEntropy()), network, loadNearestBirthday(network))
}
return openStoredWallet()
openStoredWallet()
}
suspend fun importWallet(seedPhrase: String, birthdayHeight: BlockHeight?): Initializer {
suspend fun importWallet(seedPhrase: String, birthdayHeight: BlockHeight?) {
val network = ZcashWalletApp.instance.defaultNetwork
twig("Importing ${network.networkName} wallet. Requested birthday: $birthdayHeight")
storeWallet(seedPhrase.toCharArray(), network, birthdayHeight ?: loadNearestBirthday(network))
return openStoredWallet()
storeWallet(
seedPhrase.toCharArray(),
network,
birthdayHeight ?: loadNearestBirthday(network)
)
openStoredWallet()
}
suspend fun openStoredWallet(): Initializer {
val config = loadConfig()
return ZcashWalletApp.component.initializerSubcomponent().create(config).initializer()
suspend fun openStoredWallet() {
DependenciesHolder.initializerComponent.createInitializer(loadConfig())
}
/**
@ -98,7 +94,8 @@ class WalletSetupViewModel @Inject constructor() : ViewModel() {
twig("Loading config variables")
var overwriteVks = false
val network = ZcashWalletApp.instance.defaultNetwork
val vk = loadUnifiedViewingKey() ?: onMissingViewingKey(network).also { overwriteVks = true }
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
@ -134,7 +131,6 @@ class WalletSetupViewModel @Inject constructor() : ViewModel() {
.all.map { it.key }.joinToString().let { keyNames ->
"${Const.Backup.VIEWING_KEY}, ${Const.Backup.PUBLIC_KEY}".let { missingKeys ->
// is there a typo or change in how the value is labelled?
Bugsnag.leaveBreadcrumb("One of $missingKeys not found in keySet: $keyNames")
// for troubleshooting purposes, let's see if we CAN derive the vk from the seed in these situations
var recoveryViewingKey: UnifiedViewingKey? = null
var ableToLoadSeed = false
@ -142,10 +138,11 @@ class WalletSetupViewModel @Inject constructor() : ViewModel() {
val seed = lockBox.getBytes(Const.Backup.SEED)!!
ableToLoadSeed = true
twig("Recover UVK: Seed found")
recoveryViewingKey = DerivationTool.deriveUnifiedViewingKeys(seed, network)[0]
recoveryViewingKey =
DerivationTool.deriveUnifiedViewingKeys(seed, network)[0]
twig("Recover UVK: successfully derived UVK from seed")
} catch (t: Throwable) {
Bugsnag.leaveBreadcrumb("Failed while trying to recover UVK due to: $t")
twig("Failed while trying to recover UVK due to: $t")
}
// this will happen during rare upgrade scenarios when the user migrates from a seed-only wallet to this vk-based version
@ -168,10 +165,11 @@ class WalletSetupViewModel @Inject constructor() : ViewModel() {
}
}
private suspend fun onMissingBirthday(network: ZcashNetwork): BlockHeight = failWith(InitializerException.MissingBirthdayException) {
twig("Recover Birthday: falling back to sapling birthday")
loadNearestBirthday(network)
}
private suspend fun onMissingBirthday(network: ZcashNetwork): BlockHeight =
failWith(InitializerException.MissingBirthdayException) {
twig("Recover Birthday: falling back to sapling birthday")
loadNearestBirthday(network)
}
private suspend fun loadNearestBirthday(network: ZcashNetwork) =
BlockHeight.ofLatestCheckpoint(
@ -196,7 +194,7 @@ class WalletSetupViewModel @Inject constructor() : ViewModel() {
) {
check(!lockBox.getBoolean(Const.Backup.HAS_SEED)) {
"Error! Cannot store a seed when one already exists! This would overwrite the" +
" existing seed and could lead to a loss of funds if the user has no backup!"
" existing seed and could lead to a loss of funds if the user has no backup!"
}
storeBirthday(birthday)

View File

@ -6,11 +6,11 @@ import android.view.View
import androidx.annotation.ColorRes
import androidx.core.content.ContextCompat
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import androidx.lifecycle.lifecycleScope
import androidx.viewpager2.widget.ViewPager2
import cash.z.ecc.android.R
import cash.z.ecc.android.databinding.FragmentTabLayoutBinding
import cash.z.ecc.android.di.viewmodel.viewModel
import cash.z.ecc.android.ext.onClickNavBack
import cash.z.ecc.android.feedback.Report
import cash.z.ecc.android.ui.base.BaseFragment
@ -26,7 +26,7 @@ class TabLayoutFragment :
FragmentCreator,
TabLayout.OnTabSelectedListener {
private val viewModel: ReceiveViewModel by viewModel()
private val viewModel: ReceiveViewModel by viewModels()
override fun inflate(inflater: LayoutInflater): FragmentTabLayoutBinding =
FragmentTabLayoutBinding.inflate(inflater)

View File

@ -46,11 +46,7 @@ object Deps {
val ROOM_KTX = "androidx.room:room-ktx:$version"
}
}
object Dagger : Version("2.25.2") {
val ANDROID_SUPPORT = "com.google.dagger:dagger-android-support:$version"
val ANDROID_PROCESSOR = "com.google.dagger:dagger-android-processor:$version"
val COMPILER = "com.google.dagger:dagger-compiler:$version"
}
object Google {
// solves error: Duplicate class com.google.common.util.concurrent.ListenableFuture found in modules jetified-guava-26.0-android.jar (com.google.guava:guava:26.0-android) and listenablefuture-1.0.jar (com.google.guava:listenablefuture:1.0)
// per this recommendation from Chris Povirk, given guava's decision to split ListenableFuture away from Guava: https://groups.google.com/d/msg/guava-discuss/GghaKwusjcY/bCIAKfzOEwAJ

View File

@ -1,6 +1,5 @@
package cash.z.ecc.android.feedback
//import android.util.Log
import cash.z.ecc.android.feedback.util.CompositeJob
import kotlinx.coroutines.*
import kotlinx.coroutines.channels.BroadcastChannel

View File

@ -1,16 +1,13 @@
package cash.z.ecc.android.feedback
import android.util.Log
import cash.z.ecc.android.feedback.util.CompositeJob
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import java.lang.IllegalStateException
import kotlin.coroutines.coroutineContext
/**
* Takes care of the boilerplate involved in processing feedback emissions. Simply provide callbacks
* and emissions will occur in a mutually exclusive way, across all processors, so that things like
@ -38,7 +35,6 @@ class FeedbackCoordinator(val feedback: Feedback, defaultObservers: Set<Feedback
private val jobs = CompositeJob()
val observers = mutableSetOf<FeedbackObserver>()
/**
* Wait for any in-flight listeners to complete.
*/

View File

@ -7,9 +7,8 @@ import java.nio.ByteBuffer
import java.nio.CharBuffer
import java.nio.charset.StandardCharsets
import java.util.*
import javax.inject.Inject
class LockBox @Inject constructor(private val appContext: Context) : LockBoxPlugin {
class LockBox(private val appContext: Context) : LockBoxPlugin {
private val maxLength: Int = 50

View File

@ -8,9 +8,8 @@ import cash.z.ecc.android.bip39.toEntropy
import cash.z.ecc.android.bip39.toSeed
import java.util.*
import java.util.Locale.ENGLISH
import javax.inject.Inject
class Mnemonics @Inject constructor() : MnemonicPlugin {
class Mnemonics : MnemonicPlugin {
override fun fullWordList(languageCode: String) = Mnemonics.getCachedWords(Locale.ENGLISH.language)
override fun nextEntropy(): ByteArray = WordCount.COUNT_24.toEntropy()
override fun nextMnemonic(): CharArray = MnemonicCode(WordCount.COUNT_24).chars
@ -23,4 +22,4 @@ class Mnemonics @Inject constructor() : MnemonicPlugin {
fun validate(mnemonic: CharArray) {
MnemonicCode(mnemonic).validate()
}
}
}