commit
85860ecf78
|
@ -0,0 +1,18 @@
|
|||
package cash.z.ecc.android.integration
|
||||
|
||||
import android.content.Context
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import org.junit.Before
|
||||
import org.junit.runner.RunWith
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class IntegrationTest {
|
||||
|
||||
private lateinit var appContext: Context
|
||||
|
||||
@Before
|
||||
fun start() {
|
||||
appContext = InstrumentationRegistry.getInstrumentation().targetContext
|
||||
}
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
package cash.z.ecc.android.ext
|
||||
|
||||
import android.view.View
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import kotlinx.coroutines.flow.debounce
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
|
||||
fun <T: View> LifecycleOwner.onClick(view: T, throttle: Long = 250L, block: (T) -> Unit) {
|
||||
view.clicks().debounce(throttle).onEach {
|
||||
block(view)
|
||||
}.launchIn(this.lifecycleScope)
|
||||
}
|
|
@ -3,6 +3,8 @@ package cash.z.ecc.android.ext
|
|||
import android.view.View
|
||||
import android.view.View.*
|
||||
import cash.z.ecc.android.ui.MainActivity
|
||||
import kotlinx.coroutines.channels.awaitClose
|
||||
import kotlinx.coroutines.flow.channelFlow
|
||||
|
||||
fun View.goneIf(isGone: Boolean) {
|
||||
visibility = if (isGone) GONE else VISIBLE
|
||||
|
@ -28,4 +30,13 @@ fun View.onClickNavUp() {
|
|||
"Expected MainActivity but found ${context.javaClass.simpleName}"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun View.clicks() = channelFlow<View> {
|
||||
setOnClickListener {
|
||||
offer(this@clicks)
|
||||
}
|
||||
awaitClose {
|
||||
setOnClickListener(null)
|
||||
}
|
||||
}
|
|
@ -8,7 +8,7 @@ import java.text.SimpleDateFormat
|
|||
class FeedbackFile(fileName: String = "feedback.log") :
|
||||
FeedbackCoordinator.FeedbackObserver {
|
||||
|
||||
private val file = File(ZcashWalletApp.instance.noBackupFilesDir, fileName)
|
||||
val file = File(ZcashWalletApp.instance.noBackupFilesDir, fileName)
|
||||
private val format = SimpleDateFormat("MM-dd HH:mm:ss.SSS")
|
||||
|
||||
|
||||
|
|
|
@ -50,15 +50,14 @@ class MainActivity : DaggerAppCompatActivity() {
|
|||
setContentView(R.layout.main_activity)
|
||||
initNavigation()
|
||||
|
||||
window.statusBarColor = Color.TRANSPARENT;
|
||||
window.statusBarColor = Color.TRANSPARENT
|
||||
window.navigationBarColor = Color.TRANSPARENT
|
||||
window.setFlags(
|
||||
WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS,
|
||||
WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS
|
||||
)
|
||||
setWindowFlag(
|
||||
WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS,
|
||||
false
|
||||
)// | WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION, false)
|
||||
setWindowFlag(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS, false)
|
||||
setWindowFlag(WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION, false)
|
||||
|
||||
lifecycleScope.launch {
|
||||
feedback.start()
|
||||
|
|
|
@ -1,22 +1,69 @@
|
|||
package cash.z.ecc.android.ui.detail
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
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.ext.clicks
|
||||
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 dagger.Module
|
||||
import dagger.android.ContributesAndroidInjector
|
||||
import kotlinx.coroutines.flow.debounce
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import okio.Okio
|
||||
|
||||
|
||||
class WalletDetailFragment : BaseFragment<FragmentDetailBinding>() {
|
||||
|
||||
override fun inflate(inflater: LayoutInflater): FragmentDetailBinding =
|
||||
FragmentDetailBinding.inflate(inflater)
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
binding.backButtonHitArea.onClickNavUp()
|
||||
onClick(binding.buttonLogs) {
|
||||
onViewFeedback()
|
||||
}
|
||||
onClick(binding.buttonBackup, 1L) {
|
||||
onBackupWallet()
|
||||
}
|
||||
}
|
||||
|
||||
private fun onBackupWallet() {
|
||||
mainActivity?.navController?.navigate(R.id.action_nav_detail_to_backup_wallet)
|
||||
}
|
||||
|
||||
private fun onViewFeedback() {
|
||||
loadLogFileAsText().let { logText ->
|
||||
if (logText == null) {
|
||||
mainActivity?.showSnackbar("Log file not found!")
|
||||
} else {
|
||||
val sendIntent: Intent = Intent().apply {
|
||||
action = Intent.ACTION_SEND
|
||||
putExtra(Intent.EXTRA_TEXT, logText)
|
||||
type = "text/plain"
|
||||
}
|
||||
|
||||
val shareIntent = Intent.createChooser(sendIntent, "Share Log File")
|
||||
startActivity(shareIntent)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadLogFileAsText(): String? {
|
||||
val feedbackFile: FeedbackFile =
|
||||
mainActivity?.feedbackCoordinator?.findObserver() ?: return null
|
||||
Okio.buffer(Okio.source(feedbackFile.file)).use {
|
||||
return it.readUtf8()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
package cash.z.ecc.android.ui.setup
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
import android.text.SpannableString
|
||||
import android.text.Spanned
|
||||
|
@ -7,19 +8,34 @@ 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.ZcashWalletApp
|
||||
import cash.z.ecc.android.databinding.FragmentBackupBinding
|
||||
import cash.z.ecc.android.di.annotation.FragmentScope
|
||||
import cash.z.ecc.android.ext.onClick
|
||||
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.setup.WalletSetupViewModel.WalletSetupState.SEED_WITHOUT_BACKUP
|
||||
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.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import javax.inject.Inject
|
||||
|
||||
class BackupFragment : BaseFragment<FragmentBackupBinding>() {
|
||||
@Inject
|
||||
lateinit var viewModelFactory: ViewModelProvider.Factory
|
||||
|
||||
private val walletSetup: WalletSetupViewModel by activityViewModels { viewModelFactory }
|
||||
|
||||
private var hasBackUp: Boolean? = null
|
||||
|
||||
override fun inflate(inflater: LayoutInflater): FragmentBackupBinding =
|
||||
FragmentBackupBinding.inflate(inflater)
|
||||
|
@ -41,10 +57,25 @@ class BackupFragment : BaseFragment<FragmentBackupBinding>() {
|
|||
binding.buttonPositive.setOnClickListener {
|
||||
onEnterWallet()
|
||||
}
|
||||
if (hasBackUp == true) {
|
||||
binding.buttonPositive.text = "Done"
|
||||
}
|
||||
}
|
||||
override fun onAttach(context: Context) {
|
||||
super.onAttach(context)
|
||||
walletSetup.checkSeed().onEach {
|
||||
when(it) {
|
||||
SEED_WITH_BACKUP -> {
|
||||
hasBackUp = true
|
||||
}
|
||||
}
|
||||
}.launchIn(lifecycleScope)
|
||||
}
|
||||
|
||||
private fun onEnterWallet() {
|
||||
Toast.makeText(activity, "Backup verification coming soon! For now, enjoy your new wallet!", Toast.LENGTH_LONG).show()
|
||||
if (hasBackUp != true) {
|
||||
Toast.makeText(activity, "Backup verification coming soon!", Toast.LENGTH_LONG).show()
|
||||
}
|
||||
mainActivity?.navController?.popBackStack(R.id.wallet_setup_navigation, true)
|
||||
}
|
||||
|
||||
|
|
|
@ -1,11 +1,13 @@
|
|||
package cash.z.ecc.android.ui.setup
|
||||
|
||||
import android.content.Context
|
||||
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 androidx.lifecycle.lifecycleScope
|
||||
import cash.z.ecc.android.R
|
||||
import cash.z.ecc.android.ZcashWalletApp
|
||||
import cash.z.ecc.android.databinding.FragmentLandingBinding
|
||||
|
@ -16,9 +18,13 @@ 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.android.ui.setup.WalletSetupViewModel.WalletSetupState.SEED_WITHOUT_BACKUP
|
||||
import cash.z.ecc.android.ui.setup.WalletSetupViewModel.WalletSetupState.SEED_WITH_BACKUP
|
||||
import cash.z.ecc.kotlin.mnemonic.Mnemonics
|
||||
import dagger.Module
|
||||
import dagger.android.ContributesAndroidInjector
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import javax.inject.Inject
|
||||
|
||||
class LandingFragment : BaseFragment<FragmentLandingBinding>() {
|
||||
|
@ -47,6 +53,17 @@ class LandingFragment : BaseFragment<FragmentLandingBinding>() {
|
|||
}
|
||||
}
|
||||
}
|
||||
override fun onAttach(context: Context) {
|
||||
super.onAttach(context)
|
||||
|
||||
walletSetup.checkSeed().onEach {
|
||||
when(it) {
|
||||
SEED_WITHOUT_BACKUP, SEED_WITH_BACKUP -> {
|
||||
mainActivity?.navController?.navigate(R.id.nav_backup)
|
||||
}
|
||||
}
|
||||
}.launchIn(lifecycleScope)
|
||||
}
|
||||
|
||||
private fun onSkip(count: Int) {
|
||||
when (count) {
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape
|
||||
xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<corners android:radius="16dp" />
|
||||
<stroke android:width="1dp" android:color="#282828"/>
|
||||
<solid android:color="@color/background_banner"/>
|
||||
</shape>
|
|
@ -6,6 +6,41 @@
|
|||
android:layout_height="match_parent"
|
||||
android:background="@drawable/background_home">
|
||||
|
||||
<!-- -->
|
||||
<!-- Guidelines -->
|
||||
<!-- -->
|
||||
|
||||
<!-- TODO: redo these keylines to match the designs, exactly -->
|
||||
|
||||
<androidx.constraintlayout.widget.Guideline
|
||||
android:id="@+id/guieline_bottom_buttons"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
app:layout_constraintGuide_percent="0.7017784" />
|
||||
|
||||
<androidx.constraintlayout.widget.Guideline
|
||||
android:id="@+id/guieline_keyline_start"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
app:layout_constraintGuide_percent="0.054" />
|
||||
|
||||
<androidx.constraintlayout.widget.Guideline
|
||||
android:id="@+id/guieline_keyline_end"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
app:layout_constraintGuide_percent="0.946" />
|
||||
|
||||
<androidx.constraintlayout.widget.Guideline
|
||||
android:id="@+id/guieline_keyline_bottom"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
app:layout_constraintGuide_percent="1.0" />
|
||||
|
||||
|
||||
<!-- Back Button -->
|
||||
<ImageView
|
||||
android:id="@+id/back_button"
|
||||
|
@ -33,12 +68,49 @@
|
|||
app:layout_constraintVertical_bias="0.045" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/button_backup"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="WALLET DETAIL"
|
||||
android:text="Backup\nWallet"
|
||||
android:textColor="@color/text_light"
|
||||
app:layout_constraintEnd_toEndOf="@id/guieline_keyline_end"
|
||||
app:layout_constraintTop_toTopOf="@id/back_button" />
|
||||
|
||||
<View
|
||||
android:id="@+id/text_banner_message"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:background="@drawable/background_banner_large"
|
||||
app:layout_constraintEnd_toEndOf="@id/guieline_keyline_end"
|
||||
app:layout_constraintStart_toStartOf="@id/guieline_keyline_start"
|
||||
app:layout_constraintTop_toBottomOf="@id/guieline_bottom_buttons"
|
||||
app:layout_constraintBottom_toBottomOf="@id/guieline_keyline_bottom"/>
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/button_feedback"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
style="@style/Zcash.Button.White"
|
||||
android:text="Send Feedback"
|
||||
android:padding="12dp"
|
||||
android:layout_marginEnd="24dp"
|
||||
android:layout_marginStart="24dp"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="@id/guieline_keyline_start"
|
||||
app:layout_constraintEnd_toEndOf="@id/guieline_keyline_end"
|
||||
app:layout_constraintVertical_bias="0.8"/>
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/button_logs"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
style="@style/Widget.MaterialComponents.Button.TextButton"
|
||||
android:text="View Logs"
|
||||
android:textColor="@color/text_light"
|
||||
app:layout_constraintTop_toBottomOf="@id/button_feedback"
|
||||
app:layout_constraintBottom_toBottomOf="@id/guieline_keyline_bottom"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintVertical_bias="0.2"/>
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
@ -37,7 +37,11 @@
|
|||
<fragment
|
||||
android:id="@+id/nav_detail"
|
||||
android:name="cash.z.ecc.android.ui.detail.WalletDetailFragment"
|
||||
tools:layout="@layout/fragment_detail" />
|
||||
tools:layout="@layout/fragment_detail" >
|
||||
<action
|
||||
android:id="@+id/action_nav_detail_to_backup_wallet"
|
||||
app:destination="@id/wallet_setup_navigation" />
|
||||
</fragment>
|
||||
|
||||
<include app:graph="@navigation/wallet_setup_navigation" />
|
||||
|
||||
|
|
|
@ -19,15 +19,20 @@
|
|||
<item name="windowNoTitle">true</item>
|
||||
</style>
|
||||
|
||||
<!-- Text Styles -->
|
||||
<!-- Widgets -->
|
||||
|
||||
<style name="Zcash.TextView.NumberPad" parent="Widget.AppCompat.TextView">
|
||||
<item name="android:textAppearance">@style/Zcash.TextAppearance.NumberPad</item>
|
||||
<item name="android:background">@drawable/selector_pressed_ripple_circle</item>
|
||||
</style>
|
||||
|
||||
<style name="Zcash.Button.White" parent="Widget.MaterialComponents.Button">
|
||||
<item name="backgroundTint">@android:color/white</item>
|
||||
<item name="android:textColor">@color/text_dark</item>
|
||||
</style>
|
||||
|
||||
<style name="Zcash.Button.OutlinedButton" parent="Widget.MaterialComponents.Button.OutlinedButton">
|
||||
<item name="strokeColor">@color/zcashWhite_87</item>
|
||||
<item name="strokeColor">@color/zcashWhite</item>
|
||||
</style>
|
||||
|
||||
<!-- Text Appearances -->
|
||||
|
|
|
@ -28,9 +28,7 @@ class FeedbackCoordinator(val feedback: Feedback, defaultObservers: Set<Feedback
|
|||
}
|
||||
}
|
||||
}
|
||||
if (defaultObservers.size != 3) throw IllegalStateException("BOOM")
|
||||
defaultObservers.forEach {
|
||||
Log.e("BOOM", "adding observer: $it to $feedback")
|
||||
addObserver(it)
|
||||
}
|
||||
}
|
||||
|
@ -38,7 +36,7 @@ class FeedbackCoordinator(val feedback: Feedback, defaultObservers: Set<Feedback
|
|||
private var contextMetrics = Dispatchers.IO
|
||||
private var contextActions = Dispatchers.IO
|
||||
private val jobs = CompositeJob()
|
||||
private val observers = mutableSetOf<FeedbackObserver>()
|
||||
val observers = mutableSetOf<FeedbackObserver>()
|
||||
|
||||
/**
|
||||
* Wait for any in-flight listeners to complete.
|
||||
|
@ -91,6 +89,10 @@ class FeedbackCoordinator(val feedback: Feedback, defaultObservers: Set<Feedback
|
|||
}
|
||||
}
|
||||
|
||||
inline fun <reified T: FeedbackObserver> findObserver(): T? {
|
||||
return observers.firstOrNull { it::class == T::class } as T
|
||||
}
|
||||
|
||||
private fun observeMetrics(onMetricListener: (Feedback.Metric) -> Unit) {
|
||||
feedback.metrics.onEach {
|
||||
jobs += feedback.scope.launch {
|
||||
|
|
|
@ -26,27 +26,27 @@ class LockBoxText {
|
|||
val testMessage = "Some Bytes To Test"
|
||||
val testBytes = testMessage.toByteArray()
|
||||
lockBox.setBytes("seed", testBytes)
|
||||
assertEquals(testMessage, String(lockBox.getBytes("seed")))
|
||||
assertEquals(testMessage, String(lockBox.getBytes("seed")!!))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testSeed_storeNegatives() {
|
||||
val testBytes = byteArrayOf(0x00, 0x00, -0x0F, -0x0B)
|
||||
lockBox.setBytes("seed", testBytes)
|
||||
assertTrue(testBytes.contentEquals(lockBox.getBytes("seed")))
|
||||
assertTrue(testBytes.contentEquals(lockBox.getBytes("seed")!!))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testSeed_storeLeadingZeros() {
|
||||
val testBytes = byteArrayOf(0x00, 0x00, 0x0F, 0x0B)
|
||||
lockBox.setBytes("seed", testBytes)
|
||||
assertTrue(testBytes.contentEquals(lockBox.getBytes("seed")))
|
||||
assertTrue(testBytes.contentEquals(lockBox.getBytes("seed")!!))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testPrivateKey_retrieve() {
|
||||
val testMessage = "Some Bytes To Test"
|
||||
lockBox.setCharsUtf8("spendingKey", testMessage.toCharArray())
|
||||
assertEquals(testMessage, String(lockBox.getCharsUtf8("spendingKey")))
|
||||
assertEquals(testMessage, String(lockBox.getCharsUtf8("spendingKey")!!))
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue