parent
f397260430
commit
d14637012c
|
@ -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
|
||||
|
|
|
@ -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())
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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() }
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
|
@ -1,7 +0,0 @@
|
|||
package cash.z.ecc.android.di.annotation
|
||||
|
||||
import javax.inject.Scope
|
||||
|
||||
@Scope
|
||||
@Retention(AnnotationRetention.SOURCE)
|
||||
annotation class ActivityScope
|
|
@ -1,7 +0,0 @@
|
|||
package cash.z.ecc.android.di.annotation
|
||||
|
||||
import javax.inject.Scope
|
||||
|
||||
@Scope
|
||||
@Retention(AnnotationRetention.SOURCE)
|
||||
annotation class ApplicationScope
|
|
@ -1,7 +0,0 @@
|
|||
package cash.z.ecc.android.di.annotation
|
||||
|
||||
import javax.inject.Scope
|
||||
|
||||
@Scope
|
||||
@Retention(AnnotationRetention.SOURCE)
|
||||
annotation class FragmentScope
|
|
@ -1,7 +0,0 @@
|
|||
package cash.z.ecc.android.di.annotation
|
||||
|
||||
import javax.inject.Scope
|
||||
|
||||
@Scope
|
||||
@Retention(AnnotationRetention.SOURCE)
|
||||
annotation class SynchronizerScope
|
|
@ -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>)
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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
|
|
@ -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 }
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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!")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
)
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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? {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 -> ""
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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())
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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") }
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
*/
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue