commit
e3c72936da
|
@ -11,12 +11,6 @@ import javax.inject.Inject
|
|||
|
||||
class ZcashWalletApp : DaggerApplication() {
|
||||
|
||||
@Inject
|
||||
lateinit var feedbackCoordinator: FeedbackCoordinator
|
||||
|
||||
@Inject
|
||||
lateinit var feedbackObservers: Set<@JvmSuppressWildcards FeedbackCoordinator.FeedbackObserver>
|
||||
|
||||
var creationTime: Long = 0
|
||||
private set
|
||||
|
||||
|
@ -30,7 +24,6 @@ class ZcashWalletApp : DaggerApplication() {
|
|||
|
||||
Thread.setDefaultUncaughtExceptionHandler(ExceptionReporter(Thread.getDefaultUncaughtExceptionHandler()))
|
||||
// Twig.plant(TroubleshootingTwig())
|
||||
feedbackObservers.forEach { feedbackCoordinator.addObserver(it) }
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -1,39 +1,15 @@
|
|||
package cash.z.ecc.android.di
|
||||
|
||||
import cash.z.ecc.android.feedback.*
|
||||
import android.content.Context
|
||||
import cash.z.ecc.android.ZcashWalletApp
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.multibindings.IntoSet
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Module(includes = [AppBindingModule::class])
|
||||
@Module(includes = [AppBindingModule::class, ViewModelModule::class])
|
||||
class AppModule {
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideFeedback() = Feedback()
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideFeedbackCoordinator(feedback: Feedback) = FeedbackCoordinator(feedback)
|
||||
|
||||
|
||||
//
|
||||
// Feedback Observer Set
|
||||
//
|
||||
|
||||
@Singleton
|
||||
@Provides
|
||||
@IntoSet
|
||||
fun provideFeedbackFile(): FeedbackCoordinator.FeedbackObserver = FeedbackFile()
|
||||
|
||||
@Singleton
|
||||
@Provides
|
||||
@IntoSet
|
||||
fun provideFeedbackConsole(): FeedbackCoordinator.FeedbackObserver = FeedbackConsole()
|
||||
|
||||
@Singleton
|
||||
@Provides
|
||||
@IntoSet
|
||||
fun provideFeedbackMixpanel(): FeedbackCoordinator.FeedbackObserver = FeedbackMixpanel()
|
||||
fun provideAppContext(): Context = ZcashWalletApp.instance
|
||||
}
|
||||
|
|
|
@ -0,0 +1,40 @@
|
|||
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.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
|
||||
}
|
||||
|
||||
@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
|
||||
}
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
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>)
|
|
@ -9,14 +9,23 @@ enum class NonUserAction(override val key: String, val description: String) : Fe
|
|||
override fun toString(): String = description
|
||||
}
|
||||
|
||||
enum class MetricType(override val key: String, val description: String) : Feedback.Action {
|
||||
SEED_CREATION("metric.seed.creation", "seed created")
|
||||
}
|
||||
|
||||
class LaunchMetric private constructor(private val metric: Feedback.TimeMetric) :
|
||||
Feedback.Metric by metric {
|
||||
constructor() : this(
|
||||
Feedback
|
||||
.TimeMetric("metric.app.launch", mutableListOf(ZcashWalletApp.instance.creationTime))
|
||||
.TimeMetric(
|
||||
"metric.app.launch",
|
||||
"app launched",
|
||||
mutableListOf(ZcashWalletApp.instance.creationTime)
|
||||
)
|
||||
.markTime()
|
||||
)
|
||||
override fun toString(): String {
|
||||
return "app launched in ${metric.elapsedTime}ms"
|
||||
}
|
||||
override fun toString(): String = metric.toString()
|
||||
}
|
||||
|
||||
fun <T> Feedback.measure(type: MetricType, block: () -> T) =
|
||||
this.measure(type.key, type.description, block)
|
|
@ -9,6 +9,7 @@ import android.os.Bundle
|
|||
import android.os.Vibrator
|
||||
import android.util.Log
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.WindowManager
|
||||
import android.view.inputmethod.InputMethodManager
|
||||
import android.widget.Toast
|
||||
|
@ -19,12 +20,13 @@ 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.Feedback
|
||||
import cash.z.ecc.android.feedback.LaunchMetric
|
||||
import cash.z.ecc.android.feedback.NonUserAction.FEEDBACK_STOPPED
|
||||
import cash.z.ecc.android.feedback.*
|
||||
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
|
||||
|
||||
|
@ -34,10 +36,15 @@ class MainActivity : DaggerAppCompatActivity() {
|
|||
@Inject
|
||||
lateinit var feedback: Feedback
|
||||
|
||||
@Inject
|
||||
lateinit var feedbackCoordinator: FeedbackCoordinator
|
||||
|
||||
lateinit var navController: NavController
|
||||
|
||||
private val mediaPlayer: MediaPlayer = MediaPlayer()
|
||||
|
||||
private var snackbar: Snackbar? = null
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(R.layout.main_activity)
|
||||
|
@ -53,7 +60,9 @@ class MainActivity : DaggerAppCompatActivity() {
|
|||
false
|
||||
)// | WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION, false)
|
||||
|
||||
lifecycleScope.launch { feedback.start() }
|
||||
lifecycleScope.launch {
|
||||
feedback.start()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
|
@ -70,7 +79,7 @@ class MainActivity : DaggerAppCompatActivity() {
|
|||
|
||||
override fun onDestroy() {
|
||||
lifecycleScope.launch {
|
||||
feedback.report(FEEDBACK_STOPPED)
|
||||
feedback.report(NonUserAction.FEEDBACK_STOPPED)
|
||||
feedback.stop()
|
||||
}
|
||||
super.onDestroy()
|
||||
|
@ -140,11 +149,73 @@ class MainActivity : DaggerAppCompatActivity() {
|
|||
private fun showMessage(message: String, action: String) {
|
||||
Toast.makeText(this, message, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
fun showSnackbar(message: String, action: String = "OK"): Snackbar {
|
||||
return if (snackbar == null) {
|
||||
val view = findViewById<View>(R.id.main_activity_container)
|
||||
val snacks = Snackbar
|
||||
.make(view, "$message", Snackbar.LENGTH_INDEFINITE)
|
||||
.setAction(action) { /*auto-close*/ }
|
||||
|
||||
val snackBarView = snacks.view as ViewGroup
|
||||
val navigationBarHeight = resources.getDimensionPixelSize(resources.getIdentifier("navigation_bar_height", "dimen", "android"))
|
||||
val params = snackBarView.getChildAt(0).layoutParams as ViewGroup.MarginLayoutParams
|
||||
params.setMargins(
|
||||
params.leftMargin,
|
||||
params.topMargin,
|
||||
params.rightMargin,
|
||||
navigationBarHeight
|
||||
)
|
||||
|
||||
snackBarView.getChildAt(0).setLayoutParams(params)
|
||||
snacks
|
||||
} else {
|
||||
snackbar!!.setText(message).setAction(action) {/*auto-close*/}
|
||||
}.also {
|
||||
if (!it.isShownOrQueued) it.show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Module
|
||||
abstract class MainActivityModule {
|
||||
@ActivityScope
|
||||
@ContributesAndroidInjector
|
||||
@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()
|
||||
}
|
|
@ -1,9 +1,14 @@
|
|||
package cash.z.ecc.android.ui.home
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
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.lifecycle.lifecycleScope
|
||||
import cash.z.ecc.android.R
|
||||
import cash.z.ecc.android.databinding.FragmentHomeBinding
|
||||
import cash.z.ecc.android.di.annotation.FragmentScope
|
||||
|
@ -11,14 +16,25 @@ 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.setup.WalletSetupViewModel
|
||||
import cash.z.ecc.android.ui.setup.WalletSetupViewModel.WalletSetupState.*
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import dagger.Module
|
||||
import dagger.android.ContributesAndroidInjector
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import javax.inject.Inject
|
||||
|
||||
class HomeFragment : BaseFragment<FragmentHomeBinding>() {
|
||||
|
||||
private lateinit var numberPad: List<TextView>
|
||||
|
||||
@Inject
|
||||
lateinit var viewModelFactory: ViewModelProvider.Factory
|
||||
|
||||
private val walletSetup: WalletSetupViewModel by activityViewModels { viewModelFactory }
|
||||
|
||||
override fun inflate(inflater: LayoutInflater): FragmentHomeBinding =
|
||||
FragmentHomeBinding.inflate(inflater)
|
||||
|
||||
|
@ -53,6 +69,18 @@ class HomeFragment : BaseFragment<FragmentHomeBinding>() {
|
|||
onNoFunds()
|
||||
}
|
||||
|
||||
override fun onAttach(context: Context) {
|
||||
super.onAttach(context)
|
||||
|
||||
walletSetup.checkSeed().onEach {
|
||||
when(it) {
|
||||
NO_SEED -> {
|
||||
mainActivity?.navController?.navigate(R.id.action_nav_home_to_create_wallet)
|
||||
}
|
||||
}
|
||||
}.launchIn(lifecycleScope)
|
||||
}
|
||||
|
||||
private fun onBannerAction(action: BannerAction) {
|
||||
when (action) {
|
||||
LEARN_MORE -> {
|
||||
|
|
|
@ -8,10 +8,14 @@ import android.view.View
|
|||
import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
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.lockbox.LockBox
|
||||
import cash.z.ecc.android.ui.base.BaseFragment
|
||||
import cash.z.ecc.android.ui.setup.WalletSetupViewModel.LockBoxKey
|
||||
import cash.z.ecc.android.ui.util.AddressPartNumberSpan
|
||||
import cash.z.ecc.kotlin.mnemonic.Mnemonics
|
||||
import dagger.Module
|
||||
import dagger.android.ContributesAndroidInjector
|
||||
|
||||
|
@ -24,14 +28,14 @@ class BackupFragment : BaseFragment<FragmentBackupBinding>() {
|
|||
super.onViewCreated(view, savedInstanceState)
|
||||
with(binding) {
|
||||
applySpan(
|
||||
textAddressPart1,
|
||||
textAddressPart2,
|
||||
textAddressPart3,
|
||||
textAddressPart4,
|
||||
textAddressPart5,
|
||||
textAddressPart6,
|
||||
textAddressPart7,
|
||||
textAddressPart8
|
||||
textAddressPart1, textAddressPart2, textAddressPart3,
|
||||
textAddressPart4, textAddressPart5, textAddressPart6,
|
||||
textAddressPart7, textAddressPart8, textAddressPart9,
|
||||
textAddressPart10, textAddressPart11, textAddressPart12,
|
||||
textAddressPart13, textAddressPart14, textAddressPart15,
|
||||
textAddressPart16, textAddressPart17, textAddressPart18,
|
||||
textAddressPart19, textAddressPart20, textAddressPart21,
|
||||
textAddressPart22, textAddressPart23, textAddressPart24
|
||||
)
|
||||
}
|
||||
binding.buttonPositive.setOnClickListener {
|
||||
|
@ -41,17 +45,28 @@ class BackupFragment : BaseFragment<FragmentBackupBinding>() {
|
|||
|
||||
private fun onEnterWallet() {
|
||||
Toast.makeText(activity, "Backup verification coming soon! For now, enjoy your new wallet!", Toast.LENGTH_LONG).show()
|
||||
mainActivity?.navController?.navigate(R.id.action_nav_backup_to_nav_home)
|
||||
mainActivity?.navController?.popBackStack(R.id.wallet_setup_navigation, true)
|
||||
}
|
||||
|
||||
private fun applySpan(vararg textViews: TextView) {
|
||||
val words = loadSeedWords()
|
||||
val thinSpace = "\u2005" // 0.25 em space
|
||||
textViews.forEachIndexed { index, textView ->
|
||||
textView.text = SpannableString("${index + 1}$thinSpace${textView.text}").apply {
|
||||
setSpan(AddressPartNumberSpan(), 0, 2, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
|
||||
val numLength = "$index".length
|
||||
val word = words[index]
|
||||
// TODO: work with a charsequence here, rather than constructing a String
|
||||
textView.text = SpannableString("${index + 1}$thinSpace${String(word)}").apply {
|
||||
setSpan(AddressPartNumberSpan(), 0, 1 + numLength, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadSeedWords(): List<CharArray> {
|
||||
val lockBox = LockBox(ZcashWalletApp.instance)
|
||||
val mnemonics = Mnemonics()
|
||||
val seed = lockBox.getBytes(LockBoxKey.SEED)!!
|
||||
return mnemonics.nextMnemonicList(seed)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -4,16 +4,29 @@ import android.os.Bundle
|
|||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.widget.Toast
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
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.feedback.MetricType.SEED_CREATION
|
||||
import cash.z.ecc.android.feedback.measure
|
||||
import cash.z.ecc.android.isEmulator
|
||||
import cash.z.ecc.android.lockbox.LockBox
|
||||
import cash.z.ecc.android.ui.base.BaseFragment
|
||||
import cash.z.ecc.android.ui.setup.WalletSetupViewModel.LockBoxKey
|
||||
import cash.z.ecc.kotlin.mnemonic.Mnemonics
|
||||
import dagger.Module
|
||||
import dagger.android.ContributesAndroidInjector
|
||||
import javax.inject.Inject
|
||||
|
||||
class LandingFragment : BaseFragment<FragmentLandingBinding>() {
|
||||
|
||||
@Inject
|
||||
lateinit var viewModelFactory: ViewModelProvider.Factory
|
||||
|
||||
private val walletSetup: WalletSetupViewModel by activityViewModels { viewModelFactory }
|
||||
private var skipCount: Int = 0
|
||||
|
||||
override fun inflate(inflater: LayoutInflater): FragmentLandingBinding =
|
||||
|
@ -62,6 +75,10 @@ class LandingFragment : BaseFragment<FragmentLandingBinding>() {
|
|||
}
|
||||
|
||||
private fun onNewWallet() {
|
||||
mainActivity?.feedback?.measure(SEED_CREATION) {
|
||||
walletSetup.createSeed()
|
||||
}
|
||||
|
||||
binding.textMessage.text = "Wallet created! Congratulations!"
|
||||
binding.buttonNegative.text = "Skip"
|
||||
binding.buttonPositive.text = "Backup"
|
||||
|
@ -75,9 +92,8 @@ class LandingFragment : BaseFragment<FragmentLandingBinding>() {
|
|||
}
|
||||
|
||||
private fun onEnterWallet() {
|
||||
|
||||
skipCount = 0
|
||||
mainActivity?.navController?.navigate(R.id.action_nav_landing_to_nav_home)
|
||||
mainActivity?.navController?.popBackStack()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,46 @@
|
|||
package cash.z.ecc.android.ui.setup
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import cash.z.ecc.android.lockbox.LockBox
|
||||
import cash.z.ecc.android.ui.setup.WalletSetupViewModel.LockBoxKey.HAS_BACKUP
|
||||
import cash.z.ecc.android.ui.setup.WalletSetupViewModel.LockBoxKey.HAS_SEED
|
||||
import cash.z.ecc.android.ui.setup.WalletSetupViewModel.LockBoxKey.SEED
|
||||
import cash.z.ecc.android.ui.setup.WalletSetupViewModel.WalletSetupState.*
|
||||
import cash.z.ecc.kotlin.mnemonic.Mnemonics
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import javax.inject.Inject
|
||||
|
||||
class WalletSetupViewModel @Inject constructor(val mnemonics: Mnemonics, val lockBox: LockBox) :
|
||||
ViewModel() {
|
||||
|
||||
enum class WalletSetupState {
|
||||
UNKNOWN, SEED_WITH_BACKUP, SEED_WITHOUT_BACKUP, NO_SEED
|
||||
}
|
||||
|
||||
fun checkSeed(): Flow<WalletSetupState> = flow {
|
||||
when {
|
||||
lockBox.getBoolean(HAS_BACKUP) -> emit(SEED_WITH_BACKUP)
|
||||
lockBox.getBoolean(HAS_SEED) -> emit(SEED_WITHOUT_BACKUP)
|
||||
else -> emit(NO_SEED)
|
||||
}
|
||||
}
|
||||
|
||||
fun createSeed() {
|
||||
check(!lockBox.getBoolean(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!"
|
||||
}
|
||||
|
||||
mnemonics.apply {
|
||||
lockBox.setBytes(SEED, nextSeed())
|
||||
lockBox.setBoolean(HAS_SEED, true)
|
||||
}
|
||||
}
|
||||
|
||||
object LockBoxKey {
|
||||
const val SEED = "cash.z.ecc.android.SEED1"
|
||||
const val HAS_SEED = "cash.z.ecc.android.HAS_SEED1"
|
||||
const val HAS_BACKUP = "cash.z.ecc.android.HAS_BACKUP1"
|
||||
}
|
||||
}
|
|
@ -3,5 +3,5 @@
|
|||
xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<corners android:radius="10dp" />
|
||||
<stroke android:width="1dp" android:color="#282828"/>
|
||||
<solid android:color="#171717"/>
|
||||
<solid android:color="@color/background_banner"/>
|
||||
</shape>
|
|
@ -17,6 +17,7 @@
|
|||
<!-- Address parts -->
|
||||
|
||||
<!-- Someday, there will be an advanced VirtualLayout that helps us do this without nesting but for now, this seems to be the only clean way to center all the fields -->
|
||||
<!-- its tempting to do this programmatically but for now, it's always 24 words so I'll do it statically. If this ever changes, we'll probably be using Jetpack Compose by then so it will be easier to do in code -->
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:id="@+id/receive_address_parts"
|
||||
android:layout_width="wrap_content"
|
||||
|
@ -28,50 +29,95 @@
|
|||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintBottom_toTopOf="@id/text_message"
|
||||
>
|
||||
|
||||
<!-- -->
|
||||
<!-- Column 1 -->
|
||||
<!-- -->
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_address_part_1"
|
||||
style="@style/Zcash.TextAppearance.AddressPart"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="drum"
|
||||
app:layout_constraintBottom_toTopOf="@+id/text_address_part_3"
|
||||
app:layout_constraintBottom_toTopOf="@+id/text_address_part_4"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintVertical_bias="0.0"
|
||||
app:layout_constraintVertical_chainStyle="packed" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_address_part_3"
|
||||
android:id="@+id/text_address_part_4"
|
||||
style="@style/Zcash.TextAppearance.AddressPart"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
android:text="inject"
|
||||
app:layout_constraintBottom_toTopOf="@+id/text_address_part_5"
|
||||
android:text="word"
|
||||
app:layout_constraintStart_toStartOf="@id/text_address_part_1"
|
||||
app:layout_constraintTop_toBottomOf="@id/text_address_part_1" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_address_part_5"
|
||||
style="@style/Zcash.TextAppearance.AddressPart"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
android:text="plate"
|
||||
app:layout_constraintBottom_toTopOf="@+id/text_address_part_7"
|
||||
app:layout_constraintStart_toStartOf="@id/text_address_part_1"
|
||||
app:layout_constraintTop_toBottomOf="@id/text_address_part_3" />
|
||||
|
||||
app:layout_constraintTop_toBottomOf="@id/text_address_part_1"
|
||||
app:layout_constraintBottom_toTopOf="@+id/text_address_part_7" />
|
||||
<TextView
|
||||
android:id="@+id/text_address_part_7"
|
||||
style="@style/Zcash.TextAppearance.AddressPart"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
android:text="talk"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
android:text="word"
|
||||
app:layout_constraintStart_toStartOf="@id/text_address_part_1"
|
||||
app:layout_constraintTop_toBottomOf="@id/text_address_part_5" />
|
||||
app:layout_constraintTop_toBottomOf="@id/text_address_part_4"
|
||||
app:layout_constraintBottom_toTopOf="@+id/text_address_part_10" />
|
||||
<TextView
|
||||
android:id="@+id/text_address_part_10"
|
||||
style="@style/Zcash.TextAppearance.AddressPart"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
android:text="word"
|
||||
app:layout_constraintStart_toStartOf="@id/text_address_part_1"
|
||||
app:layout_constraintTop_toBottomOf="@id/text_address_part_7"
|
||||
app:layout_constraintBottom_toTopOf="@+id/text_address_part_13" />
|
||||
<TextView
|
||||
android:id="@+id/text_address_part_13"
|
||||
style="@style/Zcash.TextAppearance.AddressPart"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
android:text="word"
|
||||
app:layout_constraintStart_toStartOf="@id/text_address_part_1"
|
||||
app:layout_constraintTop_toBottomOf="@id/text_address_part_10"
|
||||
app:layout_constraintBottom_toTopOf="@+id/text_address_part_16" />
|
||||
<TextView
|
||||
android:id="@+id/text_address_part_16"
|
||||
style="@style/Zcash.TextAppearance.AddressPart"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
android:text="word"
|
||||
app:layout_constraintStart_toStartOf="@id/text_address_part_1"
|
||||
app:layout_constraintTop_toBottomOf="@id/text_address_part_13"
|
||||
app:layout_constraintBottom_toTopOf="@+id/text_address_part_19" />
|
||||
<TextView
|
||||
android:id="@+id/text_address_part_19"
|
||||
style="@style/Zcash.TextAppearance.AddressPart"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
android:text="word"
|
||||
app:layout_constraintStart_toStartOf="@id/text_address_part_1"
|
||||
app:layout_constraintTop_toBottomOf="@id/text_address_part_16"
|
||||
app:layout_constraintBottom_toTopOf="@+id/text_address_part_22" />
|
||||
<TextView
|
||||
android:id="@+id/text_address_part_22"
|
||||
style="@style/Zcash.TextAppearance.AddressPart"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
android:text="word"
|
||||
app:layout_constraintStart_toStartOf="@id/text_address_part_1"
|
||||
app:layout_constraintTop_toBottomOf="@id/text_address_part_19" />
|
||||
|
||||
<!-- -->
|
||||
<!-- Column 2 -->
|
||||
<!-- -->
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_address_part_2"
|
||||
|
@ -81,52 +127,193 @@
|
|||
android:layout_marginStart="8dp"
|
||||
android:layout_marginLeft="8dp"
|
||||
android:text="fitness"
|
||||
app:layout_constraintBottom_toTopOf="@+id/text_address_part_4"
|
||||
app:layout_constraintStart_toEndOf="@id/barrier_left_address_column"
|
||||
app:layout_constraintBottom_toTopOf="@+id/text_address_part_5"
|
||||
app:layout_constraintStart_toEndOf="@id/barrier_left_address_column_1"
|
||||
app:layout_constraintTop_toTopOf="@id/text_address_part_1" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_address_part_4"
|
||||
style="@style/Zcash.TextAppearance.AddressPart"
|
||||
android:id="@+id/text_address_part_5"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
android:text="pool"
|
||||
app:layout_constraintBottom_toTopOf="@+id/text_address_part_6"
|
||||
style="@style/Zcash.TextAppearance.AddressPart"
|
||||
android:text="word"
|
||||
app:layout_constraintBottom_toTopOf="@+id/text_address_part_8"
|
||||
app:layout_constraintStart_toStartOf="@id/text_address_part_2"
|
||||
app:layout_constraintTop_toBottomOf="@id/text_address_part_2" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_address_part_6"
|
||||
style="@style/Zcash.TextAppearance.AddressPart"
|
||||
android:id="@+id/text_address_part_8"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
android:text="inform"
|
||||
app:layout_constraintBottom_toTopOf="@+id/text_address_part_8"
|
||||
style="@style/Zcash.TextAppearance.AddressPart"
|
||||
android:text="word"
|
||||
app:layout_constraintBottom_toTopOf="@+id/text_address_part_11"
|
||||
app:layout_constraintStart_toStartOf="@id/text_address_part_2"
|
||||
app:layout_constraintTop_toBottomOf="@id/text_address_part_4" />
|
||||
app:layout_constraintTop_toBottomOf="@id/text_address_part_5" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_address_part_8"
|
||||
style="@style/Zcash.TextAppearance.AddressPart"
|
||||
android:id="@+id/text_address_part_11"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
android:text="helmet"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
style="@style/Zcash.TextAppearance.AddressPart"
|
||||
android:text="word"
|
||||
app:layout_constraintBottom_toTopOf="@+id/text_address_part_14"
|
||||
app:layout_constraintStart_toStartOf="@id/text_address_part_2"
|
||||
app:layout_constraintTop_toBottomOf="@id/text_address_part_6" />
|
||||
app:layout_constraintTop_toBottomOf="@id/text_address_part_8" />
|
||||
|
||||
<androidx.constraintlayout.widget.Barrier
|
||||
android:id="@+id/barrier_left_address_column"
|
||||
<TextView
|
||||
android:id="@+id/text_address_part_14"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
style="@style/Zcash.TextAppearance.AddressPart"
|
||||
android:text="word"
|
||||
app:layout_constraintBottom_toTopOf="@+id/text_address_part_17"
|
||||
app:layout_constraintStart_toStartOf="@id/text_address_part_2"
|
||||
app:layout_constraintTop_toBottomOf="@id/text_address_part_11" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_address_part_17"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
style="@style/Zcash.TextAppearance.AddressPart"
|
||||
android:text="word"
|
||||
app:layout_constraintBottom_toTopOf="@+id/text_address_part_20"
|
||||
app:layout_constraintStart_toStartOf="@id/text_address_part_2"
|
||||
app:layout_constraintTop_toBottomOf="@id/text_address_part_14" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_address_part_20"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
style="@style/Zcash.TextAppearance.AddressPart"
|
||||
android:text="word"
|
||||
app:layout_constraintBottom_toTopOf="@+id/text_address_part_23"
|
||||
app:layout_constraintStart_toStartOf="@id/text_address_part_2"
|
||||
app:layout_constraintTop_toBottomOf="@id/text_address_part_17" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_address_part_23"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
style="@style/Zcash.TextAppearance.AddressPart"
|
||||
android:text="word"
|
||||
app:layout_constraintStart_toStartOf="@id/text_address_part_2"
|
||||
app:layout_constraintTop_toBottomOf="@id/text_address_part_20" />
|
||||
|
||||
<!-- -->
|
||||
<!-- Column 3 -->
|
||||
<!-- -->
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_address_part_3"
|
||||
style="@style/Zcash.TextAppearance.AddressPart"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_marginLeft="8dp"
|
||||
android:text="goals"
|
||||
app:layout_constraintBottom_toTopOf="@+id/text_address_part_6"
|
||||
app:layout_constraintStart_toEndOf="@id/barrier_left_address_column_2"
|
||||
app:layout_constraintTop_toTopOf="@id/text_address_part_1" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_address_part_6"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
style="@style/Zcash.TextAppearance.AddressPart"
|
||||
android:text="word"
|
||||
app:layout_constraintBottom_toTopOf="@+id/text_address_part_9"
|
||||
app:layout_constraintStart_toStartOf="@id/text_address_part_3"
|
||||
app:layout_constraintTop_toBottomOf="@id/text_address_part_3" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_address_part_9"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
style="@style/Zcash.TextAppearance.AddressPart"
|
||||
android:text="word"
|
||||
app:layout_constraintBottom_toTopOf="@+id/text_address_part_12"
|
||||
app:layout_constraintStart_toStartOf="@id/text_address_part_3"
|
||||
app:layout_constraintTop_toBottomOf="@id/text_address_part_6" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_address_part_12"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
style="@style/Zcash.TextAppearance.AddressPart"
|
||||
android:text="word"
|
||||
app:layout_constraintBottom_toTopOf="@+id/text_address_part_15"
|
||||
app:layout_constraintStart_toStartOf="@id/text_address_part_3"
|
||||
app:layout_constraintTop_toBottomOf="@id/text_address_part_9" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_address_part_15"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
style="@style/Zcash.TextAppearance.AddressPart"
|
||||
android:text="word"
|
||||
app:layout_constraintBottom_toTopOf="@+id/text_address_part_18"
|
||||
app:layout_constraintStart_toStartOf="@id/text_address_part_3"
|
||||
app:layout_constraintTop_toBottomOf="@id/text_address_part_12" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_address_part_18"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
style="@style/Zcash.TextAppearance.AddressPart"
|
||||
android:text="word"
|
||||
app:layout_constraintBottom_toTopOf="@+id/text_address_part_21"
|
||||
app:layout_constraintStart_toStartOf="@id/text_address_part_3"
|
||||
app:layout_constraintTop_toBottomOf="@id/text_address_part_15" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_address_part_21"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
style="@style/Zcash.TextAppearance.AddressPart"
|
||||
android:text="word"
|
||||
app:layout_constraintBottom_toTopOf="@+id/text_address_part_24"
|
||||
app:layout_constraintStart_toStartOf="@id/text_address_part_3"
|
||||
app:layout_constraintTop_toBottomOf="@id/text_address_part_18" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_address_part_24"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
style="@style/Zcash.TextAppearance.AddressPart"
|
||||
android:text="word"
|
||||
app:layout_constraintStart_toStartOf="@id/text_address_part_3"
|
||||
app:layout_constraintTop_toBottomOf="@id/text_address_part_21" />
|
||||
<!--
|
||||
text_address_part_3, text_address_part_6, text_address_part_9, text_address_part_12, text_address_part_15, text_address_part_18, text_address_part_21, text_address_part_24
|
||||
-->
|
||||
|
||||
<androidx.constraintlayout.widget.Barrier
|
||||
android:id="@+id/barrier_left_address_column_1"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginLeft="150dp"
|
||||
android:layout_marginRight="150dp"
|
||||
android:padding="150dp"
|
||||
app:barrierDirection="end"
|
||||
app:constraint_referenced_ids="text_address_part_1,text_address_part_3,text_address_part_5,text_address_part_7" />
|
||||
app:constraint_referenced_ids="text_address_part_1, text_address_part_4, text_address_part_7, text_address_part_10, text_address_part_13, text_address_part_16, text_address_part_19, text_address_part_22" />
|
||||
<androidx.constraintlayout.widget.Barrier
|
||||
android:id="@+id/barrier_left_address_column_2"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
app:barrierDirection="end"
|
||||
app:constraint_referenced_ids="text_address_part_2, text_address_part_5, text_address_part_8, text_address_part_11, text_address_part_14, text_address_part_17, text_address_part_20, text_address_part_23" />
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
<TextView
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:id="@+id/container"
|
||||
android:id="@+id/main_activity_container"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
tools:context=".ui.MainActivity">
|
||||
|
|
|
@ -4,28 +4,7 @@
|
|||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/mobile_navigation"
|
||||
app:startDestination="@+id/nav_landing">
|
||||
|
||||
<fragment
|
||||
android:id="@+id/nav_landing"
|
||||
android:name="cash.z.ecc.android.ui.setup.LandingFragment"
|
||||
tools:layout="@layout/fragment_landing" >
|
||||
<action
|
||||
android:id="@+id/action_nav_landing_to_nav_home"
|
||||
app:destination="@id/nav_home" />
|
||||
<action
|
||||
android:id="@+id/action_nav_landing_to_nav_backup"
|
||||
app:destination="@id/nav_backup" />
|
||||
</fragment>
|
||||
|
||||
<fragment
|
||||
android:id="@+id/nav_backup"
|
||||
android:name="cash.z.ecc.android.ui.setup.BackupFragment"
|
||||
tools:layout="@layout/fragment_backup" >
|
||||
<action
|
||||
android:id="@+id/action_nav_backup_to_nav_home"
|
||||
app:destination="@id/nav_home" />
|
||||
</fragment>
|
||||
app:startDestination="@+id/nav_home">
|
||||
|
||||
<fragment
|
||||
android:id="@+id/nav_home"
|
||||
|
@ -40,6 +19,9 @@
|
|||
<action
|
||||
android:id="@+id/action_nav_home_to_nav_detail"
|
||||
app:destination="@id/nav_detail" />
|
||||
<action
|
||||
android:id="@+id/action_nav_home_to_create_wallet"
|
||||
app:destination="@id/wallet_setup_navigation" />
|
||||
</fragment>
|
||||
|
||||
<fragment
|
||||
|
@ -57,4 +39,6 @@
|
|||
android:name="cash.z.ecc.android.ui.detail.WalletDetailFragment"
|
||||
tools:layout="@layout/fragment_detail" />
|
||||
|
||||
<include app:graph="@navigation/wallet_setup_navigation" />
|
||||
|
||||
</navigation>
|
|
@ -0,0 +1,24 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<navigation
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/wallet_setup_navigation"
|
||||
app:startDestination="@+id/nav_landing">
|
||||
|
||||
<fragment
|
||||
android:id="@+id/nav_landing"
|
||||
android:name="cash.z.ecc.android.ui.setup.LandingFragment"
|
||||
tools:layout="@layout/fragment_landing" >
|
||||
<action
|
||||
android:id="@+id/action_nav_landing_to_nav_backup"
|
||||
app:destination="@id/nav_backup" />
|
||||
</fragment>
|
||||
|
||||
<fragment
|
||||
android:id="@+id/nav_backup"
|
||||
android:name="cash.z.ecc.android.ui.setup.BackupFragment"
|
||||
tools:layout="@layout/fragment_backup" >
|
||||
</fragment>
|
||||
|
||||
</navigation>
|
|
@ -10,6 +10,8 @@
|
|||
<color name="colorPrimary">@color/zcashYellow</color>
|
||||
<color name="colorPrimaryDark">#4D3805</color>
|
||||
<color name="colorAccent">#A1A1A1</color>
|
||||
<color name="colorSurface">@color/text_light</color>
|
||||
<color name="colorOnSurface">@color/background_banner</color>
|
||||
|
||||
|
||||
<!-- -->
|
||||
|
@ -32,6 +34,7 @@
|
|||
<color name="zcashBlack_40">#66000000</color>
|
||||
<color name="zcashBlack_54">#8A000000</color>
|
||||
<color name="zcashBlack_87">#DD000000</color>
|
||||
<color name="zcashBlack_dark">#171717</color>
|
||||
<color name="zcashBlack_0">#00000000</color>
|
||||
|
||||
<!-- yellows -->
|
||||
|
@ -47,6 +50,8 @@
|
|||
<!-- every color here should be a reference to a palette color
|
||||
but have a more useful name for use in code -->
|
||||
|
||||
<color name="background_banner">@color/zcashBlack_dark</color>
|
||||
|
||||
<!-- text -->
|
||||
<color name="text_light">#FFFFFF</color>
|
||||
<color name="text_light_dimmed">@color/zcashWhite_50</color>
|
||||
|
|
|
@ -32,6 +32,9 @@ object Deps {
|
|||
object Google {
|
||||
const val MATERIAL = "com.google.android.material:material:1.1.0-beta01"
|
||||
}
|
||||
object JavaX {
|
||||
const val INJECT = "javax.inject:javax.inject:1"
|
||||
}
|
||||
object Kotlin : Version(kotlinVersion) {
|
||||
val STDLIB = "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$version"
|
||||
object Coroutines : Version("1.3.2") {
|
||||
|
@ -50,9 +53,7 @@ object Deps {
|
|||
}
|
||||
}
|
||||
|
||||
open class Version(@JvmField val version: String) {
|
||||
@JvmField val t = version
|
||||
}
|
||||
open class Version(@JvmField val version: String)
|
||||
|
||||
//zzz
|
||||
//zzz "androidx.constraintlayout:constraintlayout:2.0.0-alpha3"
|
||||
|
|
|
@ -92,9 +92,9 @@ class Feedback(capacity: Int = 256) {
|
|||
* will run concurrently--meaning a "happens before" relationship between the measurer and the
|
||||
* measured cannot be established and thereby the concurrent action cannot be timed.
|
||||
*/
|
||||
inline fun <T> measure(description: Any = "measurement", block: () -> T) {
|
||||
inline fun <T> measure(key: String = "measurement.generic", description: Any = "measurement", block: () -> T) {
|
||||
ensureScope()
|
||||
val metric = TimeMetric(description.toString()).markTime()
|
||||
val metric = TimeMetric(key, description.toString()).markTime()
|
||||
block()
|
||||
metric.markTime()
|
||||
report(metric)
|
||||
|
@ -151,10 +151,12 @@ class Feedback(capacity: Int = 256) {
|
|||
val startTime: Long?
|
||||
val endTime: Long?
|
||||
val elapsedTime: Long?
|
||||
val description: String
|
||||
|
||||
override fun toMap(): Map<String, Any> {
|
||||
return mapOf(
|
||||
"key" to key,
|
||||
"description" to description,
|
||||
"startTime" to (startTime ?: 0),
|
||||
"endTime" to (endTime ?: 0),
|
||||
"elapsedTime" to (elapsedTime ?: 0)
|
||||
|
@ -175,6 +177,7 @@ class Feedback(capacity: Int = 256) {
|
|||
|
||||
data class TimeMetric(
|
||||
override val key: String,
|
||||
override val description: String,
|
||||
val times: MutableList<Long> = mutableListOf()
|
||||
) : Metric {
|
||||
override val startTime: Long? get() = times.firstOrNull()
|
||||
|
@ -184,5 +187,9 @@ class Feedback(capacity: Int = 256) {
|
|||
times.add(System.currentTimeMillis())
|
||||
return this
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
return "$description in ${elapsedTime}ms"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,13 +1,16 @@
|
|||
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
|
||||
|
@ -15,7 +18,7 @@ import kotlin.coroutines.coroutineContext
|
|||
* waiting for any in-flight emissions to complete. Lastly, all monitoring will cleanly complete
|
||||
* whenever the feedback is stopped or its parent scope is cancelled.
|
||||
*/
|
||||
class FeedbackCoordinator(val feedback: Feedback) {
|
||||
class FeedbackCoordinator(val feedback: Feedback, defaultObservers: Set<FeedbackObserver> = setOf()) {
|
||||
|
||||
init {
|
||||
feedback.apply {
|
||||
|
@ -25,6 +28,11 @@ class FeedbackCoordinator(val feedback: Feedback) {
|
|||
}
|
||||
}
|
||||
}
|
||||
if (defaultObservers.size != 3) throw IllegalStateException("BOOM")
|
||||
defaultObservers.forEach {
|
||||
Log.e("BOOM", "adding observer: $it to $feedback")
|
||||
addObserver(it)
|
||||
}
|
||||
}
|
||||
|
||||
private var contextMetrics = Dispatchers.IO
|
||||
|
|
|
@ -4,39 +4,39 @@ import kotlinx.coroutines.Job
|
|||
|
||||
class CompositeJob {
|
||||
|
||||
private val jobs = mutableListOf<Job>()
|
||||
val size: Int get() = jobs.size
|
||||
private val activeJobs = mutableListOf<Job>()
|
||||
val size: Int get() = activeJobs.size
|
||||
|
||||
fun add(job: Job) {
|
||||
jobs.add(job)
|
||||
activeJobs.add(job)
|
||||
job.invokeOnCompletion {
|
||||
remove(job)
|
||||
}
|
||||
}
|
||||
|
||||
fun remove(job: Job): Boolean {
|
||||
return jobs.remove(job)
|
||||
return activeJobs.remove(job)
|
||||
}
|
||||
|
||||
fun isActive(): Boolean {
|
||||
return jobs.any { isActive() }
|
||||
return activeJobs.any { isActive() }
|
||||
}
|
||||
|
||||
suspend fun await() {
|
||||
// allow for concurrent modification since the list isn't coroutine or thread safe
|
||||
do {
|
||||
val job = jobs.firstOrNull()
|
||||
val job = activeJobs.firstOrNull()
|
||||
if (job?.isActive == true) {
|
||||
job.join()
|
||||
} else {
|
||||
// prevents an infinite loop in the extreme edge case where the list has a null item
|
||||
try { jobs.remove(job) } catch (t: Throwable) {}
|
||||
try { activeJobs.remove(job) } catch (t: Throwable) {}
|
||||
}
|
||||
} while (size > 0)
|
||||
}
|
||||
|
||||
fun cancel() {
|
||||
jobs.filter { isActive() }.forEach { it.cancel() }
|
||||
activeJobs.filter { isActive() }.forEach { it.cancel() }
|
||||
}
|
||||
|
||||
operator fun plusAssign(also: Job) {
|
||||
|
|
|
@ -37,6 +37,7 @@ android {
|
|||
}
|
||||
|
||||
dependencies {
|
||||
implementation Deps.JavaX.INJECT
|
||||
implementation Deps.Kotlin.STDLIB
|
||||
implementation Deps.AndroidX.APPCOMPAT
|
||||
implementation Deps.AndroidX.CORE_KTX
|
||||
|
|
|
@ -6,24 +6,32 @@ 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) : LockBoxProvider {
|
||||
|
||||
class LockBox(private val appContext: Context) : LockBoxProvider {
|
||||
override fun setBoolean(key: String, value: Boolean) {
|
||||
SecurePreferences.setValue(appContext, key, value)
|
||||
}
|
||||
|
||||
override fun getBoolean(key: String): Boolean {
|
||||
return SecurePreferences.getBooleanValue(appContext, key, false)
|
||||
}
|
||||
|
||||
override fun setBytes(key: String, value: ByteArray) {
|
||||
SecurePreferences.setValue(appContext, key, value.toHex())
|
||||
}
|
||||
|
||||
override fun getBytes(key: String): ByteArray {
|
||||
return SecurePreferences.getStringValue(appContext, key, null)!!.fromHex()
|
||||
override fun getBytes(key: String): ByteArray? {
|
||||
return SecurePreferences.getStringValue(appContext, key, null)?.fromHex()
|
||||
}
|
||||
|
||||
override fun setCharsUtf8(key: String, value: CharArray) {
|
||||
setBytes(key, value.toBytes())
|
||||
}
|
||||
|
||||
override fun getCharsUtf8(key: String): CharArray {
|
||||
return getBytes(key).fromBytes()
|
||||
override fun getCharsUtf8(key: String): CharArray? {
|
||||
return getBytes(key)?.fromBytes()
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -6,8 +6,11 @@ package cash.z.ecc.android.lockbox
|
|||
*/
|
||||
interface LockBoxProvider {
|
||||
fun setBytes(key: String, value: ByteArray)
|
||||
fun getBytes(key: String): ByteArray
|
||||
fun getBytes(key: String): ByteArray?
|
||||
|
||||
fun setCharsUtf8(key: String, value: CharArray)
|
||||
fun getCharsUtf8(key: String): CharArray
|
||||
fun getCharsUtf8(key: String): CharArray?
|
||||
|
||||
fun setBoolean(key: String, value: Boolean)
|
||||
fun getBoolean(key: String): Boolean
|
||||
}
|
||||
|
|
|
@ -3,6 +3,7 @@ import cash.z.ecc.android.Deps
|
|||
apply plugin: 'kotlin'
|
||||
|
||||
dependencies {
|
||||
implementation Deps.JavaX.INJECT
|
||||
implementation Deps.Kotlin.STDLIB
|
||||
|
||||
implementation 'com.madgag.spongycastle:core:1.58.0.0'
|
||||
|
|
|
@ -5,18 +5,40 @@ package cash.z.ecc.kotlin.mnemonic
|
|||
* interacts with it.
|
||||
*/
|
||||
interface MnemonicProvider {
|
||||
|
||||
/**
|
||||
* Generate a random 24-word mnemonic phrase
|
||||
* Generate a random seed.
|
||||
*/
|
||||
fun nextSeed(): ByteArray
|
||||
|
||||
/**
|
||||
* Generate a random 24-word mnemonic phrase.
|
||||
*/
|
||||
fun nextMnemonic(): CharArray
|
||||
|
||||
/**
|
||||
* Generate the 24-word mnemonic phrase corresponding to the given seed.
|
||||
*/
|
||||
fun nextMnemonic(seed: ByteArray): CharArray
|
||||
|
||||
/**
|
||||
* Generate a random 24-word mnemonic phrase, represented as a list of words.
|
||||
*/
|
||||
fun nextMnemonicList(): List<CharArray>
|
||||
|
||||
/**
|
||||
* Generate a 64-byte seed from the 24-word mnemonic phrase
|
||||
* Generate the 24-word mnemonic phrase corresponding to the given seed, represented as a list.
|
||||
*/
|
||||
fun nextMnemonicList(seed: ByteArray): List<CharArray>
|
||||
|
||||
/**
|
||||
* Generate a 64-byte seed from the 24-word mnemonic phrase.
|
||||
*/
|
||||
fun toSeed(mnemonic: CharArray): ByteArray
|
||||
|
||||
/**
|
||||
* Split the given mnemonic around spaces.
|
||||
*/
|
||||
fun toWordList(mnemonic: CharArray): List<CharArray>
|
||||
|
||||
}
|
||||
|
|
|
@ -5,28 +5,40 @@ import io.github.novacrypto.bip39.SeedCalculator
|
|||
import io.github.novacrypto.bip39.Words
|
||||
import io.github.novacrypto.bip39.wordlists.English
|
||||
import java.security.SecureRandom
|
||||
import javax.inject.Inject
|
||||
|
||||
// TODO: either find another library that allows for doing this without strings or modify this code
|
||||
// to leverage SecureCharBuffer (which doesn't work well with SeedCalculator.calculateSeed,
|
||||
// which expects a string so for that reason, we just use Strings here)
|
||||
class Mnemonics @Inject constructor(): MnemonicProvider {
|
||||
|
||||
override fun nextSeed(): ByteArray {
|
||||
return ByteArray(Words.TWENTY_FOUR.byteLength()).apply {
|
||||
SecureRandom().nextBytes(this)
|
||||
}
|
||||
}
|
||||
|
||||
class Mnemonics : MnemonicProvider {
|
||||
override fun nextMnemonic(): CharArray {
|
||||
// TODO: either find another library that allows for doing this without strings or modify this code to leverage SecureCharBuffer (which doesn't work well with SeedCalculator.calculateSeed, which expects a string so for that reason, we just use Strings here)
|
||||
return nextMnemonic(nextSeed())
|
||||
}
|
||||
|
||||
override fun nextMnemonic(seed: ByteArray): CharArray {
|
||||
return StringBuilder().let { builder ->
|
||||
ByteArray(Words.TWENTY_FOUR.byteLength()).also {
|
||||
SecureRandom().nextBytes(it)
|
||||
MnemonicGenerator(English.INSTANCE).createMnemonic(it) { c ->
|
||||
builder.append(c)
|
||||
}
|
||||
MnemonicGenerator(English.INSTANCE).createMnemonic(seed) { c ->
|
||||
builder.append(c)
|
||||
}
|
||||
builder.toString().toCharArray()
|
||||
}
|
||||
}
|
||||
|
||||
override fun nextMnemonicList(): List<CharArray> {
|
||||
return nextMnemonicList(nextSeed())
|
||||
}
|
||||
|
||||
override fun nextMnemonicList(seed: ByteArray): List<CharArray> {
|
||||
return WordListBuilder().let { builder ->
|
||||
ByteArray(Words.TWENTY_FOUR.byteLength()).also {
|
||||
SecureRandom().nextBytes(it)
|
||||
MnemonicGenerator(English.INSTANCE).createMnemonic(it) { c ->
|
||||
builder.append(c)
|
||||
}
|
||||
MnemonicGenerator(English.INSTANCE).createMnemonic(seed) { c ->
|
||||
builder.append(c)
|
||||
}
|
||||
builder.wordList
|
||||
}
|
||||
|
@ -37,11 +49,30 @@ class Mnemonics : MnemonicProvider {
|
|||
return SeedCalculator().calculateSeed(mnemonic.toString(), "")
|
||||
}
|
||||
|
||||
override fun toWordList(mnemonic: CharArray): List<CharArray> {
|
||||
val wordList = mutableListOf<CharArray>()
|
||||
var cursor = 0
|
||||
repeat(mnemonic.size) { i ->
|
||||
val isSpace = mnemonic[i] == ' '
|
||||
if (isSpace || i == (mnemonic.size - 1)) {
|
||||
val wordSize = i - cursor + if (isSpace) 0 else 1
|
||||
wordList.add(CharArray(wordSize).apply {
|
||||
repeat(wordSize) {
|
||||
this[it] = mnemonic[cursor + it]
|
||||
}
|
||||
})
|
||||
cursor = i + 1
|
||||
}
|
||||
}
|
||||
return wordList
|
||||
}
|
||||
|
||||
class WordListBuilder {
|
||||
val wordList = mutableListOf<CharArray>()
|
||||
fun append(c: CharSequence) {
|
||||
if (c[0] != English.INSTANCE.space) addWord(c)
|
||||
}
|
||||
|
||||
private fun addWord(c: CharSequence) {
|
||||
c.length.let { size ->
|
||||
val word = CharArray(size)
|
||||
|
|
|
@ -11,6 +11,7 @@ import org.junit.Assert.assertEquals
|
|||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import java.lang.Math.max
|
||||
import java.security.SecureRandom
|
||||
|
||||
|
||||
|
@ -45,6 +46,24 @@ class MnemonicTest {
|
|||
validate(words.map { String(it) })
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testMnemonic_toList() {
|
||||
val words = mnemonics.run {
|
||||
toWordList(nextMnemonic())
|
||||
}
|
||||
assertEquals(24, words.size)
|
||||
validate(words.map { String(it) })
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testMnemonic_longestWord() {
|
||||
var max = 0
|
||||
repeat(2048) {
|
||||
max = max(max, English.INSTANCE.getWord(it).length)
|
||||
}
|
||||
assertEquals(8, max)
|
||||
}
|
||||
|
||||
private fun validate(words: List<String>) {
|
||||
// return or crash!
|
||||
words.forEach { word ->
|
||||
|
|
Loading…
Reference in New Issue