Merge pull request #52 from zcash/task/fix-dagger

Task/fix dagger
This commit is contained in:
Kevin Gorham 2020-01-07 01:56:01 -05:00 committed by GitHub
commit 4ecac12f03
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
37 changed files with 604 additions and 441 deletions

View File

@ -72,7 +72,13 @@ android {
kotlinOptions {
jvmTarget = "1.8"
}
kapt {
arguments {
arg 'dagger.fastInit', 'enabled'
arg 'dagger.fullBindingGraphValidation', 'ERROR'
arg 'dagger.gradle.incremental'
}
}
applicationVariants.all { variant ->
variant.outputs.all {
outputFileName = "$archivesBaseName-v${defaultConfig.versionName}-${variant.buildType.name}.apk"
@ -96,6 +102,7 @@ dependencies {
implementation Deps.AndroidX.CORE_KTX
implementation Deps.AndroidX.CONSTRAINT_LAYOUT
implementation Deps.AndroidX.Lifecycle.LIFECYCLE_RUNTIME_KTX
implementation Deps.AndroidX.Lifecycle.LIFECYCLE_EXTENSIONS
implementation Deps.AndroidX.Navigation.FRAGMENT_KTX
implementation Deps.AndroidX.Navigation.UI_KTX
implementation "androidx.room:room-ktx:2.2.3"

View File

@ -1,18 +1,16 @@
package cash.z.ecc.android
import android.app.Application
import android.content.Context
import android.os.Build
import cash.z.ecc.android.di.DaggerAppComponent
import cash.z.ecc.android.feedback.FeedbackCoordinator
import cash.z.ecc.android.di.component.AppComponent
import cash.z.ecc.android.di.component.DaggerAppComponent
import cash.z.wallet.sdk.ext.TroubleshootingTwig
import cash.z.wallet.sdk.ext.Twig
import cash.z.wallet.sdk.ext.twig
import dagger.android.AndroidInjector
import dagger.android.DaggerApplication
import javax.inject.Inject
class ZcashWalletApp : DaggerApplication() {
class ZcashWalletApp : Application() {
var creationTime: Long = 0
private set
@ -25,17 +23,11 @@ class ZcashWalletApp : DaggerApplication() {
// Setup handler for uncaught exceptions.
super.onCreate()
component = DaggerAppComponent.factory().create(this)
Thread.setDefaultUncaughtExceptionHandler(ExceptionReporter(Thread.getDefaultUncaughtExceptionHandler()))
Twig.plant(TroubleshootingTwig())
}
/**
* Implement the HasActivityInjector behavior so that dagger knows which [AndroidInjector] to use.
*/
override fun applicationInjector(): AndroidInjector<out DaggerApplication> {
return DaggerAppComponent.factory().create(this)
}
override fun attachBaseContext(base: Context) {
super.attachBaseContext(base)
// MultiDex.install(this)
@ -43,6 +35,7 @@ class ZcashWalletApp : DaggerApplication() {
companion object {
lateinit var instance: ZcashWalletApp
lateinit var component: AppComponent
}
class ExceptionReporter(val ogHandler: Thread.UncaughtExceptionHandler) : Thread.UncaughtExceptionHandler {

View File

@ -1,8 +0,0 @@
package cash.z.ecc.android.di
import dagger.Module
@Module
abstract class AppBindingModule {
}

View File

@ -1,45 +0,0 @@
package cash.z.ecc.android.di
import cash.z.ecc.android.ZcashWalletApp
import cash.z.ecc.android.ui.MainActivityModule
import cash.z.ecc.android.ui.detail.WalletDetailFragmentModule
import cash.z.ecc.android.ui.home.HomeFragmentModule
import cash.z.ecc.android.ui.receive.ReceiveFragmentModule
import cash.z.ecc.android.ui.send.*
import cash.z.ecc.android.ui.setup.BackupFragmentModule
import cash.z.ecc.android.ui.setup.LandingFragmentModule
import dagger.BindsInstance
import dagger.Component
import dagger.android.AndroidInjector
import dagger.android.support.AndroidSupportInjectionModule
import javax.inject.Singleton
@Singleton
@Component(
modules = [
AndroidSupportInjectionModule::class,
AppModule::class,
// Activities
MainActivityModule::class,
// Fragments
HomeFragmentModule::class,
ReceiveFragmentModule::class,
SendAddressFragmentModule::class,
SendMemoFragmentModule::class,
SendConfirmFragmentModule::class,
SendFinalFragmentModule::class,
WalletDetailFragmentModule::class,
LandingFragmentModule::class,
BackupFragmentModule::class
]
)
interface AppComponent : AndroidInjector<ZcashWalletApp> {
@Component.Factory
interface Factory {
fun create(@BindsInstance application: ZcashWalletApp): AppComponent
}
}

View File

@ -1,15 +0,0 @@
package cash.z.ecc.android.di
import android.content.Context
import cash.z.ecc.android.ZcashWalletApp
import dagger.Module
import dagger.Provides
import javax.inject.Singleton
@Module(includes = [AppBindingModule::class, ViewModelModule::class])
class AppModule {
@Provides
@Singleton
fun provideAppContext(): Context = ZcashWalletApp.instance
}

View File

@ -1,52 +0,0 @@
package cash.z.ecc.android.di
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import cash.z.ecc.android.di.annotation.ViewModelKey
import cash.z.ecc.android.ui.home.HomeViewModel
import cash.z.ecc.android.ui.send.SendViewModel
import cash.z.ecc.android.ui.setup.WalletSetupViewModel
import dagger.Binds
import dagger.Module
import dagger.multibindings.IntoMap
import javax.inject.Inject
import javax.inject.Provider
import javax.inject.Singleton
@Module
abstract class ViewModelModule {
@Binds
abstract fun bindViewModelFactory(implementation: ViewModelFactory): ViewModelProvider.Factory
@Binds
@IntoMap
@ViewModelKey(WalletSetupViewModel::class)
abstract fun bindWalletSetupViewModel(implementation: WalletSetupViewModel): ViewModel
@Binds
@IntoMap
@ViewModelKey(HomeViewModel::class)
abstract fun bindHomeViewModel(implementation: HomeViewModel): ViewModel
@Binds
@IntoMap
@ViewModelKey(SendViewModel::class)
abstract fun bindSendViewModel(implementation: SendViewModel): ViewModel
}
@Singleton
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."
)
@Suppress("UNCHECKED_CAST")
return creator.get() as T
}
}

View File

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

View File

@ -0,0 +1,21 @@
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 {
// Subcomponents
fun mainActivitySubcomponent(): MainActivitySubcomponent.Factory
fun synchronizerSubcomponent(): SynchronizerSubcomponent.Factory
fun initializerSubcomponent(): InitializerSubcomponent.Factory
@Component.Factory
interface Factory {
fun create(@BindsInstance application: ZcashWalletApp): AppComponent
}
}

View File

@ -0,0 +1,22 @@
package cash.z.ecc.android.di.component
import cash.z.ecc.android.ZcashWalletApp
import cash.z.ecc.android.di.annotation.ActivityScope
import cash.z.ecc.android.di.annotation.SynchronizerScope
import cash.z.ecc.android.di.module.InitializerModule
import cash.z.wallet.sdk.Initializer
import dagger.BindsInstance
import dagger.Subcomponent
@SynchronizerScope
@Subcomponent(modules = [InitializerModule::class])
interface InitializerSubcomponent {
fun initializer(): Initializer
fun birthdayStore(): Initializer.WalletBirthdayStore
@Subcomponent.Factory
interface Factory {
fun create(@BindsInstance birthdayStore: Initializer.WalletBirthdayStore = Initializer.DefaultBirthdayStore(ZcashWalletApp.instance)): InitializerSubcomponent
}
}

View File

@ -0,0 +1,24 @@
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.ui.MainActivity
import dagger.BindsInstance
import dagger.Subcomponent
import javax.inject.Named
@ActivityScope
@Subcomponent(modules = [MainActivityModule::class])
interface MainActivitySubcomponent {
fun inject(activity: MainActivity)
@Named("BeforeSynchronizer") fun viewModelFactory(): ViewModelProvider.Factory
@Subcomponent.Factory
interface Factory {
fun create(@BindsInstance activity: FragmentActivity): MainActivitySubcomponent
}
}

View File

@ -0,0 +1,24 @@
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.wallet.sdk.Initializer
import cash.z.wallet.sdk.Synchronizer
import dagger.BindsInstance
import dagger.Subcomponent
import javax.inject.Named
@SynchronizerScope
@Subcomponent(modules = [SynchronizerModule::class])
interface SynchronizerSubcomponent {
fun synchronizer(): Synchronizer
@Named("Synchronizer") fun viewModelFactory(): ViewModelProvider.Factory
@Subcomponent.Factory
interface Factory {
fun create(@BindsInstance initializer: Initializer): SynchronizerSubcomponent
}
}

View File

@ -0,0 +1,26 @@
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.InitializerSubcomponent
import cash.z.ecc.android.di.component.MainActivitySubcomponent
import cash.z.ecc.android.di.component.SynchronizerSubcomponent
import cash.z.wallet.sdk.Initializer
import dagger.Module
import dagger.Provides
import dagger.Reusable
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
}

View File

@ -0,0 +1,17 @@
package cash.z.ecc.android.di.module
import android.content.Context
import cash.z.wallet.sdk.Initializer
import dagger.Module
import dagger.Provides
import dagger.Reusable
@Module
class InitializerModule {
private val host = "lightd-main.zecwallet.co"
private val port = 443
@Provides
@Reusable
fun provideInitializer(appContext: Context) = Initializer(appContext, host, port)
}

View File

@ -0,0 +1,44 @@
package cash.z.ecc.android.di.module
import cash.z.ecc.android.di.annotation.ActivityScope
import cash.z.ecc.android.di.component.InitializerSubcomponent
import cash.z.ecc.android.di.component.SynchronizerSubcomponent
import cash.z.ecc.android.feedback.*
import dagger.Module
import dagger.Provides
import dagger.multibindings.IntoSet
@Module(includes = [ViewModelsActivityModule::class], subcomponents = [SynchronizerSubcomponent::class, InitializerSubcomponent::class])
class MainActivityModule {
@Provides
@ActivityScope
fun provideFeedback(): Feedback = Feedback()
@Provides
@ActivityScope
fun provideFeedbackCoordinator(
feedback: Feedback,
defaultObservers: Set<@JvmSuppressWildcards FeedbackCoordinator.FeedbackObserver>
): FeedbackCoordinator = FeedbackCoordinator(feedback, defaultObservers)
//
// Default Feedback Observer Set
//
@Provides
@ActivityScope
@IntoSet
fun provideFeedbackFile(): FeedbackCoordinator.FeedbackObserver = FeedbackFile()
@Provides
@ActivityScope
@IntoSet
fun provideFeedbackConsole(): FeedbackCoordinator.FeedbackObserver = FeedbackConsole()
@Provides
@ActivityScope
@IntoSet
fun provideFeedbackMixpanel(): FeedbackCoordinator.FeedbackObserver = FeedbackMixpanel()
}

View File

@ -0,0 +1,23 @@
package cash.z.ecc.android.di.module
import android.content.Context
import cash.z.ecc.android.di.annotation.SynchronizerScope
import cash.z.wallet.sdk.Initializer
import cash.z.wallet.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 {
@Provides
@SynchronizerScope
fun provideSynchronizer(appContext: Context, initializer: Initializer): Synchronizer {
return Synchronizer(appContext, initializer)
}
}

View File

@ -0,0 +1,41 @@
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.SynchronizerScope
import cash.z.ecc.android.di.annotation.ViewModelKey
import cash.z.ecc.android.di.viewmodel.ViewModelFactory
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("BeforeSynchronizer")
@Binds
abstract fun bindViewModelFactory(viewModelFactory: ViewModelFactory): ViewModelProvider.Factory
}

View File

@ -0,0 +1,57 @@
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.ui.detail.WalletDetailViewModel
import cash.z.ecc.android.ui.home.HomeViewModel
import cash.z.ecc.android.ui.receive.ReceiveViewModel
import cash.z.ecc.android.ui.send.SendViewModel
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(WalletDetailViewModel::class)
abstract fun bindWalletDetailViewModel(implementation: WalletDetailViewModel): ViewModel
@SynchronizerScope
@Binds
@IntoMap
@ViewModelKey(ReceiveViewModel::class)
abstract fun bindReceiveViewModel(implementation: ReceiveViewModel): 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("Synchronizer")
@Binds
abstract fun bindViewModelFactory(viewModelFactory: ViewModelFactory): ViewModelProvider.Factory
}

View File

@ -0,0 +1,31 @@
package cash.z.ecc.android.di.viewmodel
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
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]
}
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!")
}

View File

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

View File

@ -13,35 +13,27 @@ import android.view.ViewGroup
import android.view.WindowManager
import android.view.inputmethod.InputMethodManager
import android.widget.Toast
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.getSystemService
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope
import androidx.navigation.NavController
import androidx.navigation.findNavController
import cash.z.ecc.android.R
import cash.z.ecc.android.ZcashWalletApp
import cash.z.ecc.android.di.annotation.ActivityScope
import cash.z.ecc.android.feedback.*
import cash.z.ecc.android.di.component.MainActivitySubcomponent
import cash.z.ecc.android.di.component.SynchronizerSubcomponent
import cash.z.ecc.android.feedback.Feedback
import cash.z.ecc.android.feedback.FeedbackCoordinator
import cash.z.ecc.android.feedback.LaunchMetric
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.ui.send.SendViewModel
import cash.z.wallet.sdk.Initializer
import cash.z.wallet.sdk.Synchronizer
import cash.z.wallet.sdk.ext.twig
import com.google.android.material.snackbar.Snackbar
import dagger.Module
import dagger.Provides
import dagger.android.ContributesAndroidInjector
import dagger.android.support.DaggerAppCompatActivity
import dagger.multibindings.IntoSet
import kotlinx.coroutines.launch
import javax.inject.Inject
class MainActivity : DaggerAppCompatActivity() {
private var syncInit: (() -> Unit)? = null
class MainActivity : AppCompatActivity() {
@Inject
lateinit var feedback: Feedback
@ -50,21 +42,23 @@ class MainActivity : DaggerAppCompatActivity() {
lateinit var feedbackCoordinator: FeedbackCoordinator
@Inject
lateinit var viewModelFactory: ViewModelProvider.Factory
lateinit var clipboard: ClipboardManager
val sendViewModel: SendViewModel by viewModels { viewModelFactory }
lateinit var navController: NavController
private val mediaPlayer: MediaPlayer = MediaPlayer()
private var snackbar: Snackbar? = null
lateinit var synchronizer: Synchronizer
lateinit var navController: NavController
lateinit var component: MainActivitySubcomponent
lateinit var synchronizerComponent: SynchronizerSubcomponent
val clipboard get() = (getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager)
override fun onCreate(savedInstanceState: Bundle?) {
component = ZcashWalletApp.component.mainActivitySubcomponent().create(this).also {
it.inject(this)
}
super.onCreate(savedInstanceState)
setContentView(R.layout.main_activity)
initNavigation()
@ -125,27 +119,10 @@ class MainActivity : DaggerAppCompatActivity() {
}
}
fun initSync() {
twig("Initializing synchronizer")
if (!::synchronizer.isInitialized) {
twig("Synchronizer didn't exist yet (this means we're opening an existing wallet). Creating it now.")
val initializer = Initializer(ZcashWalletApp.instance, "lightd-main.zecwallet.co", 443).also { it.open() }
synchronizer = Synchronizer(ZcashWalletApp.instance, initializer)
}
fun startSync(initializer: Initializer) {
synchronizerComponent = ZcashWalletApp.component.synchronizerSubcomponent().create(initializer)
feedback.report(SYNC_START)
synchronizer.start(lifecycleScope)
if (syncInit != null) {
syncInit!!()
syncInit = null
}
}
fun initializeAccount(seed: ByteArray, birthday: Initializer.WalletBirthday? = null) {
twig("Initializing accounts")
feedback.measure(Report.MetricType.ACCOUNT_CREATED) {
synchronizer =
Synchronizer(ZcashWalletApp.instance, "lightd-main.zecwallet.co", 443, seed, birthday)
}
synchronizerComponent.synchronizer().start(lifecycleScope)
}
fun playSound(fileName: String) {
@ -177,7 +154,7 @@ class MainActivity : DaggerAppCompatActivity() {
clipboard.setPrimaryClip(
ClipData.newPlainText(
"Z-Address",
synchronizer.getAddress()
synchronizerComponent.synchronizer().getAddress()
)
)
showMessage("Address copied!", "Sweet")
@ -213,56 +190,4 @@ class MainActivity : DaggerAppCompatActivity() {
if (!it.isShownOrQueued) it.show()
}
}
// TODO: refactor initialization and remove the need for this
fun onSyncInit(initBlock: () -> Unit) {
if (::synchronizer.isInitialized) {
initBlock()
} else {
syncInit = initBlock
}
}
}
@Module
abstract class MainActivityModule {
@ActivityScope
@ContributesAndroidInjector(modules = [MainActivityProviderModule::class])
abstract fun contributeActivity(): MainActivity
}
@Module
class MainActivityProviderModule {
@Provides
@ActivityScope
fun provideFeedback(): Feedback = Feedback()
@Provides
@ActivityScope
fun provideFeedbackCoordinator(
feedback: Feedback,
defaultObservers: Set<@JvmSuppressWildcards FeedbackCoordinator.FeedbackObserver>
): FeedbackCoordinator = FeedbackCoordinator(feedback, defaultObservers)
//
// Default Feedback Observer Set
//
@Provides
@ActivityScope
@IntoSet
fun provideFeedbackFile(): FeedbackCoordinator.FeedbackObserver = FeedbackFile()
@Provides
@ActivityScope
@IntoSet
fun provideFeedbackConsole(): FeedbackCoordinator.FeedbackObserver = FeedbackConsole()
@Provides
@ActivityScope
@IntoSet
fun provideFeedbackMixpanel(): FeedbackCoordinator.FeedbackObserver = FeedbackMixpanel()
}

View File

@ -5,17 +5,16 @@ import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.annotation.NonNull
import androidx.fragment.app.Fragment
import androidx.lifecycle.lifecycleScope
import androidx.viewbinding.ViewBinding
import cash.z.ecc.android.ui.MainActivity
import dagger.android.support.DaggerFragment
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
import kotlin.coroutines.coroutineContext
abstract class BaseFragment<T : ViewBinding> : DaggerFragment() {
abstract class BaseFragment<T : ViewBinding> : Fragment() {
val mainActivity: MainActivity? get() = activity as MainActivity?
lateinit var binding: T

View File

@ -8,22 +8,21 @@ import androidx.paging.PagedList
import androidx.recyclerview.widget.LinearLayoutManager
import cash.z.ecc.android.R
import cash.z.ecc.android.databinding.FragmentDetailBinding
import cash.z.ecc.android.di.annotation.FragmentScope
import cash.z.ecc.android.di.viewmodel.viewModel
import cash.z.ecc.android.ext.onClick
import cash.z.ecc.android.ext.onClickNavUp
import cash.z.ecc.android.feedback.FeedbackFile
import cash.z.ecc.android.ui.base.BaseFragment
import cash.z.wallet.sdk.entity.ConfirmedTransaction
import cash.z.wallet.sdk.ext.collectWith
import cash.z.wallet.sdk.ext.twig
import dagger.Module
import dagger.android.ContributesAndroidInjector
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch
import okio.Okio
class WalletDetailFragment : BaseFragment<FragmentDetailBinding>() {
private val viewModel: WalletDetailViewModel by viewModel()
private lateinit var adapter: TransactionAdapter<ConfirmedTransaction>
override fun inflate(inflater: LayoutInflater): FragmentDetailBinding =
@ -53,9 +52,7 @@ class WalletDetailFragment : BaseFragment<FragmentDetailBinding>() {
binding.recyclerTransactions.layoutManager =
LinearLayoutManager(activity, LinearLayoutManager.VERTICAL, false)
adapter = TransactionAdapter()
resumedScope.launch {
mainActivity?.synchronizer?.clearedTransactions?.collect { onTransactionsUpdated(it) }
}
viewModel.transactions.collectWith(resumedScope) { onTransactionsUpdated(it) }
binding.recyclerTransactions.adapter = adapter
}
@ -96,12 +93,4 @@ class WalletDetailFragment : BaseFragment<FragmentDetailBinding>() {
return it.readUtf8()
}
}
}
@Module
abstract class WalletDetailFragmentModule {
@FragmentScope
@ContributesAndroidInjector
abstract fun contributeFragment(): WalletDetailFragment
}

View File

@ -0,0 +1,19 @@
package cash.z.ecc.android.ui.detail
import androidx.lifecycle.ViewModel
import cash.z.wallet.sdk.Synchronizer
import cash.z.wallet.sdk.ext.twig
import javax.inject.Inject
class WalletDetailViewModel @Inject constructor() : ViewModel() {
@Inject
lateinit var synchronizer: Synchronizer
val transactions get() = synchronizer.clearedTransactions
override fun onCleared() {
super.onCleared()
twig("WalletDetailViewModel cleared!")
}
}

View File

@ -6,44 +6,38 @@ import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.widget.TextView
import androidx.fragment.app.activityViewModels
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope
import cash.z.ecc.android.R
import cash.z.ecc.android.databinding.FragmentHomeBinding
import cash.z.ecc.android.di.annotation.FragmentScope
import cash.z.ecc.android.di.viewmodel.activityViewModel
import cash.z.ecc.android.di.viewmodel.viewModel
import cash.z.ecc.android.ext.disabledIf
import cash.z.ecc.android.ext.goneIf
import cash.z.ecc.android.ext.onClickNavTo
import cash.z.ecc.android.ui.base.BaseFragment
import cash.z.ecc.android.ui.home.HomeFragment.BannerAction.*
import cash.z.ecc.android.ui.send.SendViewModel
import cash.z.ecc.android.ui.setup.WalletSetupViewModel
import cash.z.ecc.android.ui.setup.WalletSetupViewModel.WalletSetupState.NO_SEED
import cash.z.wallet.sdk.SdkSynchronizer
import cash.z.wallet.sdk.Synchronizer.Status.SYNCING
import cash.z.wallet.sdk.ext.convertZatoshiToZecString
import cash.z.wallet.sdk.ext.convertZecToZatoshi
import cash.z.wallet.sdk.ext.safelyConvertToBigDecimal
import cash.z.wallet.sdk.ext.twig
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import dagger.Module
import dagger.android.ContributesAndroidInjector
import kotlinx.coroutines.channels.ConflatedBroadcastChannel
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import javax.inject.Inject
class HomeFragment : BaseFragment<FragmentHomeBinding>() {
private lateinit var numberPad: List<TextView>
private lateinit var uiModel: HomeViewModel.UiModel
@Inject
lateinit var viewModelFactory: ViewModelProvider.Factory
private val walletSetup: WalletSetupViewModel by activityViewModels { viewModelFactory }
private val viewModel: HomeViewModel by activityViewModels { viewModelFactory }
private val walletSetup: WalletSetupViewModel by activityViewModel(false)
private val sendViewModel: SendViewModel by activityViewModel()
private val viewModel: HomeViewModel by viewModel()
private val _typedChars = ConflatedBroadcastChannel<Char>()
private val typedChars = _typedChars.asFlow()
@ -60,20 +54,17 @@ class HomeFragment : BaseFragment<FragmentHomeBinding>() {
twig("HomeFragment.onAttach")
super.onAttach(context)
// call initSync either now or later (after initializing DBs with newly created seed)
// this will call startSync either now or later (after initializing with newly created seed)
walletSetup.checkSeed().onEach {
twig("Checking seed")
when(it) {
NO_SEED -> {
twig("Seed not found, therefore, launching seed creation flow")
// interact with user to create, backup and verify seed
mainActivity?.navController?.navigate(R.id.action_nav_home_to_create_wallet)
// leads to a call to initSync(), later (after accounts are created from seed)
}
else -> {
twig("Found seed. Re-opening existing wallet")
mainActivity?.initSync()
}
if (it == NO_SEED) {
// interact with user to create, backup and verify seed
// leads to a call to startSync(), later (after accounts are created from seed)
twig("Seed not found, therefore, launching seed creation flow")
mainActivity?.navController?.navigate(R.id.action_nav_home_to_create_wallet)
} else {
twig("Found seed. Re-opening existing wallet")
mainActivity?.startSync(walletSetup.openWallet())
}
}.launchIn(lifecycleScope)
}
@ -108,19 +99,15 @@ class HomeFragment : BaseFragment<FragmentHomeBinding>() {
onSend()
}
}
if (::uiModel.isInitialized) {
twig("uiModel exists!")
onModelUpdated(HomeViewModel.UiModel(), uiModel)
} else {
twig("uiModel does not exist!")
mainActivity?.onSyncInit {
viewModel.initialize(mainActivity!!.synchronizer, typedChars)
}
}
// if (::uiModel.isInitialized) {
// twig("uiModel exists!")
// onModelUpdated(HomeViewModel.UiModel(), uiModel)
// }
}
override fun onResume() {
super.onResume()
viewModel.initialize(typedChars)
twig("HomeFragment.onResume resumeScope.isActive: ${resumedScope.isActive} $resumedScope")
viewModel.uiModels.scanReduce { old, new ->
onModelUpdated(old, new)
@ -134,7 +121,7 @@ class HomeFragment : BaseFragment<FragmentHomeBinding>() {
// but for some reason, this doesn't always happen, which kind of defeats the purpose
// of having a cold stream in the view model
resumedScope.launch {
(mainActivity!!.synchronizer as SdkSynchronizer).refreshBalance()
viewModel.refreshBalance()
}
}
@ -178,7 +165,7 @@ class HomeFragment : BaseFragment<FragmentHomeBinding>() {
fun setSendAmount(amount: String) {
binding.textSendAmount.text = "\$$amount"
mainActivity?.sendViewModel?.zatoshiAmount = amount.safelyConvertToBigDecimal().convertZecToZatoshi()
sendViewModel.zatoshiAmount = amount.safelyConvertToBigDecimal().convertZecToZatoshi()
binding.buttonSend.disabledIf(amount == "0")
}
@ -343,12 +330,4 @@ class HomeFragment : BaseFragment<FragmentHomeBinding>() {
super.onDetach()
twig("HomeFragment.onDetach")
}
}
@Module
abstract class HomeFragmentModule {
@FragmentScope
@ContributesAndroidInjector
abstract fun contributeFragment(): HomeFragment
}

View File

@ -2,26 +2,37 @@ package cash.z.ecc.android.ui.home
import android.os.Parcelable
import androidx.lifecycle.ViewModel
import cash.z.wallet.sdk.SdkSynchronizer
import cash.z.wallet.sdk.Synchronizer
import cash.z.wallet.sdk.Synchronizer.Status.DISCONNECTED
import cash.z.wallet.sdk.Synchronizer.Status.SYNCED
import cash.z.wallet.sdk.ext.ZcashSdk
import cash.z.wallet.sdk.ext.ZcashSdk.MINERS_FEE_ZATOSHI
import cash.z.wallet.sdk.ext.ZcashSdk.ZATOSHI_PER_ZEC
import cash.z.wallet.sdk.ext.twig
import kotlinx.android.parcel.Parcelize
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.conflate
import kotlinx.coroutines.flow.scan
import javax.inject.Inject
class HomeViewModel @Inject constructor() : ViewModel() {
@Inject
lateinit var synchronizer: Synchronizer
lateinit var uiModels: Flow<UiModel>
var initialized = false
fun initialize(
synchronizer: Synchronizer,
typedChars: Flow<Char>
) {
twig("init called")
if (initialized) {
twig("Warning already initialized HomeViewModel. Ignoring call to initialize.")
return
}
val zec = typedChars.scan("0") { acc, c ->
when {
// no-op cases
@ -56,6 +67,10 @@ class HomeViewModel @Inject constructor() : ViewModel() {
twig("HomeViewModel cleared!")
}
suspend fun refreshBalance() {
(synchronizer as SdkSynchronizer).refreshBalance()
}
@Parcelize
data class UiModel( // <- THIS ERROR IS AN IDE BUG WITH PARCELIZE
val status: Synchronizer.Status = DISCONNECTED,

View File

@ -8,31 +8,28 @@ import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.widget.TextView
import androidx.lifecycle.lifecycleScope
import cash.z.android.qrecycler.QRecycler
import cash.z.ecc.android.databinding.FragmentReceiveBinding
import cash.z.ecc.android.di.annotation.FragmentScope
import cash.z.ecc.android.ext.onClickNavTo
import cash.z.ecc.android.di.viewmodel.viewModel
import cash.z.ecc.android.ext.onClickNavUp
import cash.z.ecc.android.ui.base.BaseFragment
import cash.z.ecc.android.ui.util.AddressPartNumberSpan
import cash.z.wallet.sdk.ext.twig
import dagger.Module
import dagger.android.ContributesAndroidInjector
import kotlinx.android.synthetic.main.fragment_receive.*
import kotlinx.coroutines.launch
import kotlin.math.floor
import kotlin.math.round
import kotlin.math.roundToInt
class ReceiveFragment : BaseFragment<FragmentReceiveBinding>() {
override fun inflate(inflater: LayoutInflater): FragmentReceiveBinding =
FragmentReceiveBinding.inflate(inflater)
private val viewModel: ReceiveViewModel by viewModel()
lateinit var qrecycler: QRecycler
lateinit var addressParts: Array<TextView>
override fun inflate(inflater: LayoutInflater): FragmentReceiveBinding =
FragmentReceiveBinding.inflate(inflater)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
addressParts = arrayOf(
@ -56,9 +53,7 @@ class ReceiveFragment : BaseFragment<FragmentReceiveBinding>() {
override fun onResume() {
super.onResume()
resumedScope.launch {
mainActivity?.synchronizer?.getAddress()?.let { address ->
onAddressLoaded(address)
}
onAddressLoaded(viewModel.getAddress())
}
}
@ -97,12 +92,4 @@ class ReceiveFragment : BaseFragment<FragmentReceiveBinding>() {
addressParts[index].text = textSpan
}
}
@Module
abstract class ReceiveFragmentModule {
@FragmentScope
@ContributesAndroidInjector
abstract fun contributeFragment(): ReceiveFragment
}

View File

@ -0,0 +1,19 @@
package cash.z.ecc.android.ui.receive
import androidx.lifecycle.ViewModel
import cash.z.wallet.sdk.Synchronizer
import cash.z.wallet.sdk.ext.twig
import javax.inject.Inject
class ReceiveViewModel @Inject constructor() : ViewModel() {
@Inject
lateinit var synchronizer: Synchronizer
suspend fun getAddress(): String = synchronizer.getAddress()
override fun onCleared() {
super.onCleared()
twig("WalletDetailViewModel cleared!")
}
}

View File

@ -9,6 +9,7 @@ import android.view.inputmethod.EditorInfo
import cash.z.ecc.android.R
import cash.z.ecc.android.databinding.FragmentSendAddressBinding
import cash.z.ecc.android.di.annotation.FragmentScope
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.ui.base.BaseFragment
@ -16,10 +17,13 @@ import cash.z.wallet.sdk.ext.convertZatoshiToZecString
import cash.z.wallet.sdk.ext.twig
import dagger.Module
import dagger.android.ContributesAndroidInjector
import javax.inject.Inject
class SendAddressFragment : BaseFragment<FragmentSendAddressBinding>(),
ClipboardManager.OnPrimaryClipChangedListener {
val sendViewModel: SendViewModel by viewModel()
override fun inflate(inflater: LayoutInflater): FragmentSendAddressBinding =
FragmentSendAddressBinding.inflate(inflater)
@ -35,7 +39,7 @@ class SendAddressFragment : BaseFragment<FragmentSendAddressBinding>(),
binding.textBannerMessage.setOnClickListener {
onPaste()
}
binding.textAmount.text = "Sending ${mainActivity?.sendViewModel?.zatoshiAmount.convertZatoshiToZecString(8)} ZEC"
binding.textAmount.text = "Sending ${sendViewModel.zatoshiAmount.convertZatoshiToZecString(8)} ZEC"
binding.inputZcashAddress.setOnEditorActionListener { v, actionId, event ->
if (actionId == EditorInfo.IME_ACTION_DONE) {
onAddAddress()
@ -47,7 +51,7 @@ class SendAddressFragment : BaseFragment<FragmentSendAddressBinding>(),
}
private fun onAddAddress() {
mainActivity?.sendViewModel?.toAddress = binding.inputZcashAddress.text.toString()
sendViewModel.toAddress = binding.inputZcashAddress.text.toString()
mainActivity?.navController?.navigate(R.id.action_nav_send_address_to_send_memo)
}
@ -102,12 +106,4 @@ class SendAddressFragment : BaseFragment<FragmentSendAddressBinding>(),
private fun ClipboardManager.text(): CharSequence =
primaryClip!!.getItemAt(0).coerceToText(mainActivity)
}
@Module
abstract class SendAddressFragmentModule {
@FragmentScope
@ContributesAndroidInjector
abstract fun contributeFragment(): SendAddressFragment
}
}

View File

@ -6,48 +6,38 @@ import android.view.View
import androidx.lifecycle.lifecycleScope
import cash.z.ecc.android.R
import cash.z.ecc.android.databinding.FragmentSendConfirmBinding
import cash.z.ecc.android.di.annotation.FragmentScope
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.ui.MainActivity
import cash.z.ecc.android.ui.base.BaseFragment
import cash.z.wallet.sdk.ext.abbreviatedAddress
import cash.z.wallet.sdk.ext.convertZatoshiToZecString
import dagger.Module
import dagger.android.ContributesAndroidInjector
import kotlinx.coroutines.launch
class SendConfirmFragment : BaseFragment<FragmentSendConfirmBinding>() {
val sendViewModel: SendViewModel by viewModel()
override fun inflate(inflater: LayoutInflater): FragmentSendConfirmBinding =
FragmentSendConfirmBinding.inflate(inflater)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
mainActivity?.apply {
binding.buttonNext.setOnClickListener {
onSend()
}
binding.backButtonHitArea.onClickNavBack()
mainActivity?.lifecycleScope?.launch {
binding.textConfirmation.text =
"Send ${sendViewModel?.zatoshiAmount.convertZatoshiToZecString(8)} ZEC to ${sendViewModel?.toAddress.abbreviatedAddress()}?"
}
sendViewModel.memo.trim().isNotEmpty().let { hasMemo ->
binding.radioIncludeAddress.isChecked = hasMemo
binding.radioIncludeAddress.goneIf(!hasMemo)
}
binding.buttonNext.setOnClickListener {
onSend()
}
binding.backButtonHitArea.onClickNavBack()
mainActivity?.lifecycleScope?.launch {
binding.textConfirmation.text =
"Send ${sendViewModel.zatoshiAmount.convertZatoshiToZecString(8)} ZEC to ${sendViewModel?.toAddress.abbreviatedAddress()}?"
}
sendViewModel.memo.trim().isNotEmpty().let { hasMemo ->
binding.radioIncludeAddress.isChecked = hasMemo
binding.radioIncludeAddress.goneIf(!hasMemo)
}
}
private fun onSend() {
mainActivity?.navController?.navigate(R.id.action_nav_send_confirm_to_send_final)
}
}
@Module
abstract class SendConfirmFragmentModule {
@FragmentScope
@ContributesAndroidInjector
abstract fun contributeFragment(): SendConfirmFragment
}
}

View File

@ -7,23 +7,23 @@ import android.view.View
import androidx.lifecycle.lifecycleScope
import cash.z.ecc.android.R
import cash.z.ecc.android.databinding.FragmentSendFinalBinding
import cash.z.ecc.android.di.annotation.FragmentScope
import cash.z.ecc.android.di.viewmodel.viewModel
import cash.z.ecc.android.ext.goneIf
import cash.z.ecc.android.ui.base.BaseFragment
import cash.z.wallet.sdk.entity.*
import cash.z.wallet.sdk.ext.abbreviatedAddress
import cash.z.wallet.sdk.ext.convertZatoshiToZecString
import cash.z.wallet.sdk.ext.twig
import dagger.Module
import dagger.android.ContributesAndroidInjector
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlin.math.min
import kotlin.random.Random
class SendFinalFragment : BaseFragment<FragmentSendFinalBinding>() {
val sendViewModel: SendViewModel by viewModel()
override fun inflate(inflater: LayoutInflater): FragmentSendFinalBinding =
FragmentSendFinalBinding.inflate(inflater)
@ -36,8 +36,8 @@ class SendFinalFragment : BaseFragment<FragmentSendFinalBinding>() {
onExit()
}
binding.textConfirmation.text =
"Sending ${mainActivity?.sendViewModel?.zatoshiAmount.convertZatoshiToZecString(8)} ZEC to ${mainActivity?.sendViewModel?.toAddress?.abbreviatedAddress()}"
mainActivity?.sendViewModel?.memo?.trim()?.isNotEmpty()?.let { hasMemo ->
"Sending ${sendViewModel.zatoshiAmount.convertZatoshiToZecString(8)} ZEC to ${sendViewModel.toAddress.abbreviatedAddress()}"
sendViewModel.memo?.trim()?.isNotEmpty()?.let { hasMemo ->
binding.radioIncludeAddress.isChecked = hasMemo
binding.radioIncludeAddress.goneIf(!hasMemo)
}
@ -46,7 +46,7 @@ class SendFinalFragment : BaseFragment<FragmentSendFinalBinding>() {
override fun onAttach(context: Context) {
super.onAttach(context)
mainActivity?.apply {
sendViewModel.send(synchronizer).onEach {
sendViewModel.send().onEach {
onPendingTxUpdated(it)
}.launchIn(mainActivity?.lifecycleScope!!)
}
@ -92,12 +92,4 @@ class SendFinalFragment : BaseFragment<FragmentSendFinalBinding>() {
private fun onExit() {
mainActivity?.navController?.popBackStack(R.id.send_navigation, true)
}
}
@Module
abstract class SendFinalFragmentModule {
@FragmentScope
@ContributesAndroidInjector
abstract fun contributeFragment(): SendFinalFragment
}
}

View File

@ -6,14 +6,14 @@ import android.view.View
import android.view.inputmethod.EditorInfo
import cash.z.ecc.android.R
import cash.z.ecc.android.databinding.FragmentSendMemoBinding
import cash.z.ecc.android.di.annotation.FragmentScope
import cash.z.ecc.android.di.viewmodel.viewModel
import cash.z.ecc.android.ext.onClickNavBack
import cash.z.ecc.android.ext.onClickNavUp
import cash.z.ecc.android.ui.base.BaseFragment
import dagger.Module
import dagger.android.ContributesAndroidInjector
class SendMemoFragment : BaseFragment<FragmentSendMemoBinding>() {
val sendViewModel: SendViewModel by viewModel()
override fun inflate(inflater: LayoutInflater): FragmentSendMemoBinding =
FragmentSendMemoBinding.inflate(inflater)
@ -24,7 +24,7 @@ class SendMemoFragment : BaseFragment<FragmentSendMemoBinding>() {
}
binding.buttonSkip.setOnClickListener {
binding.inputMemo.setText("")
mainActivity?.sendViewModel?.memo = ""
sendViewModel.memo = ""
mainActivity?.navController?.navigate(R.id.action_nav_send_memo_to_send_confirm)
}
binding.backButtonHitArea.onClickNavBack()
@ -47,15 +47,7 @@ class SendMemoFragment : BaseFragment<FragmentSendMemoBinding>() {
}
private fun onAddMemo() {
mainActivity?.sendViewModel?.memo = binding.inputMemo.text.toString()
sendViewModel.memo = binding.inputMemo.text.toString()
mainActivity?.navController?.navigate(R.id.action_nav_send_memo_to_send_confirm)
}
}
@Module
abstract class SendMemoFragmentModule {
@FragmentScope
@ContributesAndroidInjector
abstract fun contributeFragment(): SendMemoFragment
}
}

View File

@ -3,7 +3,7 @@ package cash.z.ecc.android.ui.send
import androidx.lifecycle.ViewModel
import cash.z.ecc.android.lockbox.LockBox
import cash.z.ecc.android.ui.setup.WalletSetupViewModel
import cash.z.wallet.sdk.SdkSynchronizer
import cash.z.wallet.sdk.Initializer
import cash.z.wallet.sdk.Synchronizer
import cash.z.wallet.sdk.entity.PendingTransaction
import cash.z.wallet.sdk.ext.twig
@ -11,9 +11,19 @@ import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.onEach
import javax.inject.Inject
class SendViewModel @Inject constructor(var lockBox: LockBox) : ViewModel() {
fun send(synchronizer: Synchronizer): Flow<PendingTransaction> {
val keys = (synchronizer as SdkSynchronizer).rustBackend!!.deriveSpendingKeys(
class SendViewModel @Inject constructor() : ViewModel() {
@Inject
lateinit var lockBox: LockBox
@Inject
lateinit var synchronizer: Synchronizer
@Inject
lateinit var initializer: Initializer
fun send(): Flow<PendingTransaction> {
val keys = initializer.deriveSpendingKeys(
lockBox.getBytes(WalletSetupViewModel.LockBoxKey.SEED)!!
)
return synchronizer.sendToAddress(

View File

@ -8,13 +8,13 @@ import android.view.LayoutInflater
import android.view.View
import android.widget.TextView
import android.widget.Toast
import androidx.fragment.app.activityViewModels
import androidx.lifecycle.ViewModelProvider
import androidx.activity.addCallback
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.annotation.FragmentScope
import cash.z.ecc.android.di.viewmodel.activityViewModel
import cash.z.ecc.android.di.viewmodel.viewModel
import cash.z.ecc.android.feedback.Report.MetricType.SEED_PHRASE_LOADED
import cash.z.ecc.android.feedback.measure
import cash.z.ecc.android.lockbox.LockBox
@ -23,20 +23,14 @@ import cash.z.ecc.android.ui.setup.WalletSetupViewModel.LockBoxKey
import cash.z.ecc.android.ui.setup.WalletSetupViewModel.WalletSetupState.SEED_WITH_BACKUP
import cash.z.ecc.android.ui.util.AddressPartNumberSpan
import cash.z.ecc.kotlin.mnemonic.Mnemonics
import dagger.Module
import dagger.android.ContributesAndroidInjector
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import javax.inject.Inject
class BackupFragment : BaseFragment<FragmentBackupBinding>() {
@Inject
lateinit var viewModelFactory: ViewModelProvider.Factory
private val walletSetup: WalletSetupViewModel by activityViewModels { viewModelFactory }
val walletSetup: WalletSetupViewModel by activityViewModel(false)
private var hasBackUp: Boolean? = null
@ -64,6 +58,13 @@ class BackupFragment : BaseFragment<FragmentBackupBinding>() {
binding.buttonPositive.text = "Done"
}
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
mainActivity?.onBackPressedDispatcher?.addCallback(this) {
onEnterWallet(false)
}
}
override fun onAttach(context: Context) {
super.onAttach(context)
walletSetup.checkSeed().onEach {
@ -75,8 +76,8 @@ class BackupFragment : BaseFragment<FragmentBackupBinding>() {
}.launchIn(lifecycleScope)
}
private fun onEnterWallet() {
if (hasBackUp != true) {
private fun onEnterWallet(showMessage: Boolean = this.hasBackUp != true) {
if (showMessage) {
Toast.makeText(activity, "Backup verification coming soon!", Toast.LENGTH_LONG).show()
}
mainActivity?.navController?.popBackStack(R.id.wallet_setup_navigation, true)
@ -104,12 +105,4 @@ class BackupFragment : BaseFragment<FragmentBackupBinding>() {
result
}
}
}
@Module
abstract class BackupFragmentModule {
@FragmentScope
@ContributesAndroidInjector
abstract fun contributeFragment(): BackupFragment
}

View File

@ -6,31 +6,25 @@ import android.view.LayoutInflater
import android.view.View
import android.widget.Toast
import androidx.fragment.app.activityViewModels
import androidx.lifecycle.ViewModelProvider
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.annotation.FragmentScope
import cash.z.ecc.android.isEmulator
import cash.z.ecc.android.di.viewmodel.activityViewModel
import cash.z.ecc.android.di.viewmodel.viewModel
import cash.z.ecc.android.ui.base.BaseFragment
import cash.z.ecc.android.ui.setup.WalletSetupViewModel.WalletSetupState.SEED_WITHOUT_BACKUP
import cash.z.ecc.android.ui.setup.WalletSetupViewModel.WalletSetupState.SEED_WITH_BACKUP
import cash.z.wallet.sdk.Initializer
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import dagger.Module
import dagger.android.ContributesAndroidInjector
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import javax.inject.Inject
class LandingFragment : BaseFragment<FragmentLandingBinding>() {
@Inject
lateinit var viewModelFactory: ViewModelProvider.Factory
private val walletSetup: WalletSetupViewModel by activityViewModel(false)
private val walletSetup: WalletSetupViewModel by activityViewModels { viewModelFactory }
private var skipCount: Int = 0
override fun inflate(inflater: LayoutInflater): FragmentLandingBinding =
@ -103,19 +97,14 @@ class LandingFragment : BaseFragment<FragmentLandingBinding>() {
Toast.makeText(activity, "Coming soon!", Toast.LENGTH_SHORT).show()
}
// AKA import wallet
private fun onUseDevWallet() {
val 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"
val 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"
val birthday = 663174//626599
mainActivity?.apply {
lifecycleScope.launch {
initializeAccount(
walletSetup.importWallet(feedback, seedPhrase.toCharArray()),
Initializer.loadBirthdayFromAssets(ZcashWalletApp.instance, birthday)
)
initSync()
mainActivity?.startSync(walletSetup.importWallet(seedPhrase, birthday))
}
binding.buttonPositive.isEnabled = true
binding.textMessage.text = "Wallet imported! Congratulations!"
binding.buttonNegative.text = "Skip"
@ -125,17 +114,13 @@ class LandingFragment : BaseFragment<FragmentLandingBinding>() {
}
}
// TODO: move this to the ViewModel but doing so requires fixing dagger so do that as a separate PR
private fun onNewWallet() {
lifecycleScope.launch {
val ogText = binding.buttonPositive.text
binding.buttonPositive.text = "creating"
binding.buttonPositive.isEnabled = false
mainActivity?.apply {
initializeAccount(walletSetup.createWallet(feedback))
initSync()
}
mainActivity?.startSync(walletSetup.newWallet())
binding.buttonPositive.isEnabled = true
binding.textMessage.text = "Wallet created! Congratulations!"
@ -155,11 +140,4 @@ class LandingFragment : BaseFragment<FragmentLandingBinding>() {
skipCount = 0
mainActivity?.navController?.popBackStack()
}
}
@Module
abstract class LandingFragmentModule {
@FragmentScope
@ContributesAndroidInjector
abstract fun contributeFragment(): LandingFragment
}

View File

@ -1,20 +1,32 @@
package cash.z.ecc.android.ui.setup
import androidx.lifecycle.ViewModel
import cash.z.ecc.android.ZcashWalletApp
import cash.z.ecc.android.feedback.Feedback
import cash.z.ecc.android.feedback.Report
import cash.z.ecc.android.feedback.Report.MetricType.*
import cash.z.ecc.android.feedback.measure
import cash.z.ecc.android.lockbox.LockBox
import cash.z.ecc.android.ui.setup.WalletSetupViewModel.WalletSetupState.*
import cash.z.ecc.kotlin.mnemonic.Mnemonics
import cash.z.wallet.sdk.Initializer
import cash.z.wallet.sdk.ext.twig
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.withContext
import javax.inject.Inject
class WalletSetupViewModel @Inject constructor(val mnemonics: Mnemonics, val lockBox: LockBox) :
ViewModel() {
class WalletSetupViewModel @Inject constructor() : ViewModel() {
@Inject
lateinit var mnemonics: Mnemonics
@Inject
lateinit var lockBox: LockBox
@Inject
lateinit var feedback: Feedback
enum class WalletSetupState {
UNKNOWN, SEED_WITH_BACKUP, SEED_WITHOUT_BACKUP, NO_SEED
@ -28,12 +40,42 @@ class WalletSetupViewModel @Inject constructor(val mnemonics: Mnemonics, val loc
}
}
/**
* Re-open an existing wallet. This is the most common use case, where a user has previously
* created or imported their seed and is returning to the wallet. In other words, this is the
* non-FTUE case.
*/
fun openWallet(): Initializer {
twig("Opening existing wallet")
return ZcashWalletApp.component.initializerSubcomponent().create().run {
initializer().open(birthdayStore().getBirthday())
}
}
suspend fun newWallet(): Initializer {
twig("Initializing new wallet")
return ZcashWalletApp.component.initializerSubcomponent().create().run {
initializer().apply {
new(createWallet(), birthdayStore().newWalletBirthday)
}
}
}
suspend fun importWallet(seedPhrase: String, birthdayHeight: Int): Initializer {
twig("Importing wallet")
return ZcashWalletApp.component.initializerSubcomponent().create(Initializer.DefaultBirthdayStore(ZcashWalletApp.instance, birthdayHeight)).run {
initializer().apply {
import(importWallet(seedPhrase.toCharArray()), birthdayStore().getBirthday())
}
}
}
/**
* Take all the steps necessary to create a new wallet and measure how long it takes.
*
* @param feedback the object used for measurement.
*/
suspend fun createWallet(feedback: Feedback): ByteArray = withContext(Dispatchers.IO){
private suspend fun createWallet(): ByteArray = withContext(Dispatchers.IO){
check(!lockBox.getBoolean(LockBoxKey.HAS_SEED)) {
"Error! Cannot create 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!"
@ -64,8 +106,7 @@ class WalletSetupViewModel @Inject constructor(val mnemonics: Mnemonics, val loc
*
* @param feedback the object used for measurement.
*/
suspend fun importWallet(
feedback: Feedback,
private suspend fun importWallet(
seedPhrase: CharArray
): ByteArray = withContext(Dispatchers.IO) {
check(!lockBox.getBoolean(LockBoxKey.HAS_SEED)) {
@ -89,8 +130,6 @@ class WalletSetupViewModel @Inject constructor(val mnemonics: Mnemonics, val loc
}
}
object LockBoxKey {
const val SEED = "cash.z.ecc.android.SEED"
const val SEED_PHRASE = "cash.z.ecc.android.SEED_PHRASE"

View File

@ -22,6 +22,7 @@ object Deps {
}
object Lifecycle: Version("2.2.0-rc02") {
val LIFECYCLE_RUNTIME_KTX = "androidx.lifecycle:lifecycle-runtime-ktx:$version"
val LIFECYCLE_EXTENSIONS = "androidx.lifecycle:lifecycle-extensions:$version"
}
}
object Dagger : Version("2.25.2") {

View File

@ -19,3 +19,5 @@ android.useAndroidX=true
android.enableJetifier=true
# Kotlin code style for this project: "official" or "obsolete":
kotlin.code.style=official
dagger.fastInit=enabled