* Add ktlint

Upgrade Gradle
Fix formatting for ktlint

* Upgrade to Gradle 7.0

Bump up NDK, targetSDKVersion & kotlinVersion

Fix expected type issue in ScanFragment getPackageInfo()

* Revert SDK version.

Delete unused file SendAddressFragment.kt
Fix comment formatting.

Co-authored-by: Kevin Gorham <kevin.gorham@electriccoin.co>
This commit is contained in:
Adí 2021-05-03 11:22:16 -04:00 committed by GitHub
parent 3a45ac5db2
commit 695bffab9a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
75 changed files with 485 additions and 466 deletions

14
.editorconfig Normal file
View File

@ -0,0 +1,14 @@
# Comma-separated list of rules to disable (Since 0.34.0)
# Note that rules in any ruleset other than the standard ruleset will need to be prefixed
# by the ruleset identifier.
disabled_rules=import-ordering,no-wildcard-imports
# Defines the imports layout. The layout can be composed by the following symbols:
# "*" - wildcard. There must be at least one entry of a single wildcard to match all other imports. Matches anything after a specified symbol/import as well.
# "|" - blank line. Supports only single blank lines between imports. No blank line is allowed in the beginning or end of the layout.
# "^" - alias import, e.g. "^android.*" will match all android alias imports, "^" will match all other alias imports.
# import paths - these can be full paths, e.g. "java.util.List.*" as well as wildcard paths, e.g. "kotlin.**"
# Examples (we use ij_kotlin_imports_layout to set an imports layout for both ktlint and IDEA via a single property):
ij_kotlin_imports_layout=* # alphabetical with capital letters before lower case letters (e.g. Z before a), no blank lines
ij_kotlin_imports_layout=*,java.**,javax.**,kotlin.**,^ # default IntelliJ IDEA style, same as alphabetical, but with "java", "javax", "kotlin" and alias imports in the end of the imports list
ij_kotlin_imports_layout=android.**,|,^org.junit.**,kotlin.io.Closeable.*,|,*,^ # custom imports layout

View File

@ -184,4 +184,8 @@ dependencies {
} }
defaultTasks 'clean', 'assembleZcashmainnetRelease' defaultTasks 'clean', 'assembleZcashmainnetRelease'
apply from: "$rootDir/ktlint.gradle"
preBuild.dependsOn('ktlintFormat')
preBuild.dependsOn('ktlint')

View File

@ -1,16 +1,16 @@
package cash.z.ecc.android package cash.z.ecc.android
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.Parameterized
import cash.z.ecc.android.ui.util.MemoUtil import cash.z.ecc.android.ui.util.MemoUtil
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertEquals import org.junit.Assert.assertEquals
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.Parameterized
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
//@RunWith(Parameterized::class) // @RunWith(Parameterized::class)
class MemoTest(val input: String, val output: String) { class MemoTest(val input: String, val output: String) {
@Test @Test
@ -42,4 +42,4 @@ class MemoTest(val input: String, val output: String) {
) )
) )
} }
} }

View File

@ -14,7 +14,6 @@ class ConversionsTest {
@Before @Before
fun setUp() { fun setUp() {
} }
@Test @Test
@ -73,7 +72,6 @@ class ConversionsTest {
Assert.assertEquals(1000, result.longValueExact()) Assert.assertEquals(1000, result.longValueExact())
} }
@Test @Test
fun testToBigDecimal_thousandCommaWithDecimal() { fun testToBigDecimal_thousandCommaWithDecimal() {
val input = "1,000.00" val input = "1,000.00"
@ -109,12 +107,6 @@ class ConversionsTest {
Assert.assertEquals(1, result.longValueExact()) Assert.assertEquals(1, result.longValueExact())
} }
@Test @Test
fun testToZecString_full() { fun testToZecString_full() {
val input = 112_341_123L val input = 112_341_123L
@ -149,4 +141,4 @@ class ConversionsTest {
val result = WalletZecFormmatter.toZecStringFull(input) val result = WalletZecFormmatter.toZecStringFull(input)
Assert.assertEquals("1.12350004", result) Assert.assertEquals("1.12350004", result)
} }
} }

View File

@ -4,7 +4,6 @@ import android.content.Context
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry import androidx.test.platform.app.InstrumentationRegistry
import cash.z.ecc.android.lockbox.LockBox import cash.z.ecc.android.lockbox.LockBox
import cash.z.ecc.kotlin.mnemonic.Mnemonics
import cash.z.ecc.android.sdk.Initializer import cash.z.ecc.android.sdk.Initializer
import cash.z.ecc.android.sdk.type.ZcashNetwork import cash.z.ecc.android.sdk.type.ZcashNetwork
import okio.Buffer import okio.Buffer
@ -24,7 +23,7 @@ class IntegrationTest {
private val mnemonics = Mnemonics() private val mnemonics = Mnemonics()
private val phrase = private val phrase =
"human pulse approve subway climb stairs mind gentle raccoon warfare fog roast sponsor" + "human pulse approve subway climb stairs mind gentle raccoon warfare fog roast sponsor" +
" under absorb spirit hurdle animal original honey owner upper empower describe" " under absorb spirit hurdle animal original honey owner upper empower describe"
@Before @Before
fun start() { fun start() {
@ -37,7 +36,7 @@ class IntegrationTest {
assertEquals( assertEquals(
"Generated incorrect BIP-39 seed!", "Generated incorrect BIP-39 seed!",
"f4e3d38d9c244da7d0407e19a93c80429614ee82dcf62c141235751c9f1228905d12a1f275f" + "f4e3d38d9c244da7d0407e19a93c80429614ee82dcf62c141235751c9f1228905d12a1f275f" +
"5c22f6fb7fcd9e0a97f1676e0eec53fdeeeafe8ce8aa39639b9fe", "5c22f6fb7fcd9e0a97f1676e0eec53fdeeeafe8ce8aa39639b9fe",
seed.toHex() seed.toHex()
) )
} }
@ -72,10 +71,10 @@ class IntegrationTest {
acceptedSize-- acceptedSize--
} }
val maxSeedPhraseLength = 8 * 24 + 23 //215 (max length of each word is 8) val maxSeedPhraseLength = 8 * 24 + 23 // 215 (max length of each word is 8)
assertTrue( assertTrue(
"LockBox does not support the maximum length seed phrase." + "LockBox does not support the maximum length seed phrase." +
" Expected: $maxSeedPhraseLength but was: $acceptedSize", " Expected: $maxSeedPhraseLength but was: $acceptedSize",
acceptedSize > maxSeedPhraseLength acceptedSize > maxSeedPhraseLength
) )
} }
@ -94,7 +93,6 @@ class IntegrationTest {
initializer.erase() initializer.erase()
} }
private fun ByteArray.toHex(): String { private fun ByteArray.toHex(): String {
val sb = StringBuilder(size * 2) val sb = StringBuilder(size * 2)
for (b in this) for (b in this)
@ -117,4 +115,4 @@ class IntegrationTest {
.map { allowedChars.random() } .map { allowedChars.random() }
.joinToString("") .joinToString("")
} }
} }

View File

@ -29,7 +29,7 @@ class LockBoxTest {
lockBox["longStr"] = sampleHex lockBox["longStr"] = sampleHex
val actual: String = lockBox["longStr"]!! val actual: String = lockBox["longStr"]!!
if(sampleHex == actual) successCount++ if (sampleHex == actual) successCount++
lockBox.clear() lockBox.clear()
} }
assertEquals(iterations, successCount) assertEquals(iterations, successCount)
@ -43,7 +43,7 @@ class LockBoxTest {
lockBox["shortStr"] = sampleHex lockBox["shortStr"] = sampleHex
val actual: String = lockBox["shortStr"]!! val actual: String = lockBox["shortStr"]!!
if(sampleHex == actual) successCount++ if (sampleHex == actual) successCount++
lockBox.clear() lockBox.clear()
} }
assertEquals(iterations, successCount) assertEquals(iterations, successCount)
@ -57,7 +57,7 @@ class LockBoxTest {
lockBox["giantStr"] = sampleHex lockBox["giantStr"] = sampleHex
val actual: String = lockBox["giantStr"]!! val actual: String = lockBox["giantStr"]!!
if(sampleHex == actual) successCount++ if (sampleHex == actual) successCount++
lockBox.clear() lockBox.clear()
} }
assertEquals(iterations, successCount) assertEquals(iterations, successCount)

View File

@ -11,10 +11,13 @@ import cash.z.ecc.android.ext.tryWithWarning
import cash.z.ecc.android.feedback.FeedbackCoordinator import cash.z.ecc.android.feedback.FeedbackCoordinator
import cash.z.ecc.android.sdk.ext.twig import cash.z.ecc.android.sdk.ext.twig
import cash.z.ecc.android.sdk.type.ZcashNetwork import cash.z.ecc.android.sdk.type.ZcashNetwork
import kotlinx.coroutines.* import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import javax.inject.Inject import javax.inject.Inject
class ZcashWalletApp : Application(), CameraXConfig.Provider { class ZcashWalletApp : Application(), CameraXConfig.Provider {
@Inject @Inject
@ -102,10 +105,9 @@ class ZcashWalletApp : Application(), CameraXConfig.Provider {
} }
} }
fun ZcashWalletApp.isEmulator(): Boolean { fun ZcashWalletApp.isEmulator(): Boolean {
val goldfish = Build.HARDWARE.contains("goldfish"); val goldfish = Build.HARDWARE.contains("goldfish")
val emu = (System.getProperty("ro.kernel.qemu", "")?.length ?: 0) > 0; val emu = (System.getProperty("ro.kernel.qemu", "")?.length ?: 0) > 0
val sdk = Build.MODEL.toLowerCase().contains("sdk") val sdk = Build.MODEL.toLowerCase().contains("sdk")
return goldfish || emu || sdk; return goldfish || emu || sdk
} }

View File

@ -11,4 +11,4 @@ import kotlin.reflect.KClass
) )
@Retention(AnnotationRetention.RUNTIME) @Retention(AnnotationRetention.RUNTIME)
@MapKey @MapKey
annotation class ViewModelKey(val value: KClass<out ViewModel>) annotation class ViewModelKey(val value: KClass<out ViewModel>)

View File

@ -20,4 +20,4 @@ interface AppComponent {
interface Factory { interface Factory {
fun create(@BindsInstance application: ZcashWalletApp): AppComponent fun create(@BindsInstance application: ZcashWalletApp): AppComponent
} }
} }

View File

@ -17,4 +17,4 @@ interface InitializerSubcomponent {
interface Factory { interface Factory {
fun create(@BindsInstance config: Initializer.Config): InitializerSubcomponent fun create(@BindsInstance config: Initializer.Config): InitializerSubcomponent
} }
} }

View File

@ -22,4 +22,4 @@ interface MainActivitySubcomponent {
interface Factory { interface Factory {
fun create(@BindsInstance activity: FragmentActivity): MainActivitySubcomponent fun create(@BindsInstance activity: FragmentActivity): MainActivitySubcomponent
} }
} }

View File

@ -22,4 +22,4 @@ interface SynchronizerSubcomponent {
interface Factory { interface Factory {
fun create(@BindsInstance initializer: Initializer): SynchronizerSubcomponent fun create(@BindsInstance initializer: Initializer): SynchronizerSubcomponent
} }
} }

View File

@ -5,10 +5,14 @@ import android.content.Context
import cash.z.ecc.android.ZcashWalletApp import cash.z.ecc.android.ZcashWalletApp
import cash.z.ecc.android.di.component.MainActivitySubcomponent import cash.z.ecc.android.di.component.MainActivitySubcomponent
import cash.z.ecc.android.ext.Const import cash.z.ecc.android.ext.Const
import cash.z.ecc.android.feedback.* import cash.z.ecc.android.feedback.Feedback
import cash.z.ecc.android.feedback.FeedbackBugsnag
import cash.z.ecc.android.feedback.FeedbackConsole
import cash.z.ecc.android.feedback.FeedbackCoordinator
import cash.z.ecc.android.feedback.FeedbackFile
import cash.z.ecc.android.feedback.FeedbackMixpanel
import cash.z.ecc.android.lockbox.LockBox import cash.z.ecc.android.lockbox.LockBox
import cash.z.ecc.android.sdk.ext.SilentTwig import cash.z.ecc.android.sdk.ext.SilentTwig
import cash.z.ecc.android.sdk.ext.TroubleshootingTwig
import cash.z.ecc.android.sdk.ext.Twig import cash.z.ecc.android.sdk.ext.Twig
import cash.z.ecc.android.ui.util.DebugFileTwig import cash.z.ecc.android.ui.util.DebugFileTwig
import dagger.Module import dagger.Module
@ -35,12 +39,10 @@ class AppModule {
return LockBox(appContext) return LockBox(appContext)
} }
// //
// Feedback // Feedback
// //
@Provides @Provides
@Singleton @Singleton
fun provideFeedback(): Feedback = Feedback() fun provideFeedback(): Feedback = Feedback()
@ -59,7 +61,6 @@ class AppModule {
} }
} }
// //
// Default Feedback Observer Set // Default Feedback Observer Set
// //

View File

@ -8,6 +8,4 @@ import dagger.Module
includes = [ViewModelsActivityModule::class], includes = [ViewModelsActivityModule::class],
subcomponents = [SynchronizerSubcomponent::class, InitializerSubcomponent::class] subcomponents = [SynchronizerSubcomponent::class, InitializerSubcomponent::class]
) )
class MainActivityModule { class MainActivityModule
}

View File

@ -19,5 +19,4 @@ class SynchronizerModule {
fun provideSynchronizer(appContext: Context, initializer: Initializer): Synchronizer { fun provideSynchronizer(appContext: Context, initializer: Initializer): Synchronizer {
return Synchronizer(initializer) return Synchronizer(initializer)
} }
} }

View File

@ -1,6 +1,5 @@
package cash.z.ecc.android.di.module package cash.z.ecc.android.di.module
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
import cash.z.ecc.android.di.annotation.ActivityScope import cash.z.ecc.android.di.annotation.ActivityScope
@ -26,7 +25,6 @@ abstract class ViewModelsActivityModule {
@ViewModelKey(WalletSetupViewModel::class) @ViewModelKey(WalletSetupViewModel::class)
abstract fun bindWalletSetupViewModel(implementation: WalletSetupViewModel): ViewModel abstract fun bindWalletSetupViewModel(implementation: WalletSetupViewModel): ViewModel
/** /**
* Factory for view models that are created until before the Synchronizer exists. This is a * 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 * little tricky because we cannot make them all in one place or else they won't be available
@ -38,5 +36,4 @@ abstract class ViewModelsActivityModule {
@Named(Const.Name.BEFORE_SYNCHRONIZER) @Named(Const.Name.BEFORE_SYNCHRONIZER)
@Binds @Binds
abstract fun bindViewModelFactory(viewModelFactory: ViewModelFactory): ViewModelProvider.Factory abstract fun bindViewModelFactory(viewModelFactory: ViewModelFactory): ViewModelProvider.Factory
}
}

View File

@ -1,6 +1,5 @@
package cash.z.ecc.android.di.module package cash.z.ecc.android.di.module
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
import cash.z.ecc.android.di.annotation.SynchronizerScope import cash.z.ecc.android.di.annotation.SynchronizerScope
@ -65,7 +64,7 @@ abstract class ViewModelsSynchronizerModule {
@IntoMap @IntoMap
@ViewModelKey(SettingsViewModel::class) @ViewModelKey(SettingsViewModel::class)
abstract fun bindSettingsViewModel(implementation: SettingsViewModel): ViewModel abstract fun bindSettingsViewModel(implementation: SettingsViewModel): ViewModel
/** /**
* Factory for view models that are not created until the Synchronizer exists. Only VMs that * 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 * require the Synchronizer should wait until it is created. In other words, these are the VMs
@ -75,4 +74,4 @@ abstract class ViewModelsSynchronizerModule {
@Named(Const.Name.SYNCHRONIZER) @Named(Const.Name.SYNCHRONIZER)
@Binds @Binds
abstract fun bindViewModelFactory(viewModelFactory: ViewModelFactory): ViewModelProvider.Factory abstract fun bindViewModelFactory(viewModelFactory: ViewModelFactory): ViewModelProvider.Factory
} }

View File

@ -5,7 +5,6 @@ import androidx.lifecycle.ViewModelProvider
import cash.z.ecc.android.ui.MainActivity import cash.z.ecc.android.ui.MainActivity
import cash.z.ecc.android.ui.base.BaseFragment import cash.z.ecc.android.ui.base.BaseFragment
inline fun <reified VM : ViewModel> BaseFragment<*>.viewModel() = object : Lazy<VM> { inline fun <reified VM : ViewModel> BaseFragment<*>.viewModel() = object : Lazy<VM> {
val cached: VM? = null val cached: VM? = null
override fun isInitialized(): Boolean = cached != null override fun isInitialized(): Boolean = cached != null
@ -58,4 +57,4 @@ inline fun <reified VM : ViewModel> MainActivity.activityViewModel() = object :
} }
} }
} }
} }

View File

@ -13,9 +13,9 @@ class ViewModelFactory @Inject constructor(
modelClass.isAssignableFrom(it.key) modelClass.isAssignableFrom(it.key)
}?.value ?: throw IllegalArgumentException( }?.value ?: throw IllegalArgumentException(
"No map entry found for ${modelClass.canonicalName}. Verify that this ViewModel has" + "No map entry found for ${modelClass.canonicalName}. Verify that this ViewModel has" +
" been added to the ViewModelModule. ${creators.keys}" " been added to the ViewModelModule. ${creators.keys}"
) )
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
return creator.get() as T return creator.get() as T
} }
} }

View File

@ -5,15 +5,12 @@ import cash.z.ecc.android.ext.ConversionsUniform.LONG_SCALE
import cash.z.ecc.android.ext.ConversionsUniform.SHORT_FORMATTER import cash.z.ecc.android.ext.ConversionsUniform.SHORT_FORMATTER
import cash.z.ecc.android.sdk.ext.Conversions import cash.z.ecc.android.sdk.ext.Conversions
import cash.z.ecc.android.sdk.ext.ZcashSdk import cash.z.ecc.android.sdk.ext.ZcashSdk
import cash.z.ecc.android.sdk.ext.convertZatoshiToZec
import cash.z.ecc.android.sdk.ext.toZec
import java.math.BigDecimal import java.math.BigDecimal
import java.math.MathContext import java.math.MathContext
import java.math.RoundingMode import java.math.RoundingMode
import java.text.DecimalFormat import java.text.DecimalFormat
import java.text.NumberFormat import java.text.NumberFormat
import java.util.* import java.util.Locale
/** /**
* Do the necessary conversions in one place * Do the necessary conversions in one place
@ -75,5 +72,4 @@ object WalletZecFormmatter {
BigDecimal(this ?: 0L, MathContext.DECIMAL128) BigDecimal(this ?: 0L, MathContext.DECIMAL128)
.divide(ConversionsUniform.ONE_ZEC_IN_ZATOSHI) .divide(ConversionsUniform.ONE_ZEC_IN_ZATOSHI)
.setScale(LONG_SCALE, ConversionsUniform.roundingMode) .setScale(LONG_SCALE, ConversionsUniform.roundingMode)
}
}

View File

@ -9,7 +9,6 @@ import androidx.core.content.getSystemService
import cash.z.ecc.android.R import cash.z.ecc.android.R
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
fun Context.showClearDataConfirmation(onDismiss: () -> Unit = {}, onCancel: () -> Unit = {}): Dialog { fun Context.showClearDataConfirmation(onDismiss: () -> Unit = {}, onCancel: () -> Unit = {}): Dialog {
return MaterialAlertDialogBuilder(this) return MaterialAlertDialogBuilder(this)
.setTitle(R.string.dialog_nuke_wallet_title) .setTitle(R.string.dialog_nuke_wallet_title)
@ -39,10 +38,13 @@ fun Context.showUninitializedError(error: Throwable? = null, onDismiss: () -> Un
if (error != null) throw error if (error != null) throw error
} }
.setNegativeButton(getString(R.string.dialog_error_uninitialized_button_negative)) { dialog, _ -> .setNegativeButton(getString(R.string.dialog_error_uninitialized_button_negative)) { dialog, _ ->
showClearDataConfirmation(onDismiss, onCancel = { showClearDataConfirmation(
// do not let the user back into the app because we cannot recover from this case onDismiss,
showUninitializedError(error, onDismiss) onCancel = {
}) // do not let the user back into the app because we cannot recover from this case
showUninitializedError(error, onDismiss)
}
)
} }
.show() .show()
} }
@ -187,4 +189,4 @@ fun Context.showSharedLibraryCriticalError(e: Throwable): Dialog = showCriticalM
titleResId = R.string.dialog_error_critical_link_title, titleResId = R.string.dialog_error_critical_link_title,
messageResId = R.string.dialog_error_critical_link_message, messageResId = R.string.dialog_error_critical_link_message,
onDismiss = { throw e } onDismiss = { throw e }
) )

View File

@ -55,7 +55,7 @@ inline fun EditText.limitDecimalPlaces(max: Int) {
// Restore the cursor position // Restore the cursor position
if (oldText != textStr) { if (oldText != textStr) {
val cursorPosition = editText.selectionEnd; val cursorPosition = editText.selectionEnd
editText.setText(textStr) editText.setText(textStr)
editText.setSelection( editText.setSelection(
(cursorPosition - (oldText.length - textStr.length)).coerceIn( (cursorPosition - (oldText.length - textStr.length)).coerceIn(
@ -68,12 +68,10 @@ inline fun EditText.limitDecimalPlaces(max: Int) {
} }
override fun onTextChanged(p0: CharSequence?, p1: Int, p2: Int, p3: Int) { override fun onTextChanged(p0: CharSequence?, p1: Int, p2: Int, p3: Int) {
} }
}) })
} }
fun TextView.convertZecToZatoshi(): Long? { fun TextView.convertZecToZatoshi(): Long? {
return try { return try {
text.toString().safelyConvertToBigDecimal()?.convertZecToZatoshi() ?: null text.toString().safelyConvertToBigDecimal()?.convertZecToZatoshi() ?: null

View File

@ -7,7 +7,7 @@ import cash.z.ecc.android.sdk.ext.Bush
import cash.z.ecc.android.sdk.ext.Twig import cash.z.ecc.android.sdk.ext.Twig
import cash.z.ecc.android.sdk.ext.twig import cash.z.ecc.android.sdk.ext.twig
import cash.z.ecc.android.sdk.type.WalletBalance import cash.z.ecc.android.sdk.type.WalletBalance
import java.util.* import java.util.Locale
import kotlin.math.roundToInt import kotlin.math.roundToInt
/** /**
@ -67,4 +67,4 @@ inline fun Context.locale(): Locale {
inline fun <reified T> Twig.find(): T? { inline fun <reified T> Twig.find(): T? {
return if (Bush.trunk::class.java.isAssignableFrom(T::class.java)) Bush.trunk as T return if (Bush.trunk::class.java.isAssignableFrom(T::class.java)) Bush.trunk as T
else null else null
} }

View File

@ -32,16 +32,15 @@ internal inline fun @receiver:StringRes Int.toAppStringFormatted(vararg formatAr
return ZcashWalletApp.instance.getString(this, *formatArgs) return ZcashWalletApp.instance.getString(this, *formatArgs)
} }
/** /**
* Grab an integer from the application resources * Grab an integer from the application resources
*/ */
internal inline fun @receiver:IntegerRes Int.toAppInt(): Int { internal inline fun @receiver:IntegerRes Int.toAppInt(): Int {
return ZcashWalletApp.instance.resources.getInteger(this)} return ZcashWalletApp.instance.resources.getInteger(this)
}
fun Float.toPx() = this * Resources.getSystem().displayMetrics.density fun Float.toPx() = this * Resources.getSystem().displayMetrics.density
fun Int.toPx() = (this * Resources.getSystem().displayMetrics.density + 0.5f).toInt() fun Int.toPx() = (this * Resources.getSystem().displayMetrics.density + 0.5f).toInt()
fun Int.toDp() = (this / Resources.getSystem().displayMetrics.density + 0.5f).toInt() fun Int.toDp() = (this / Resources.getSystem().displayMetrics.density + 0.5f).toInt()

View File

@ -7,8 +7,8 @@ import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
fun <T: View> LifecycleOwner.onClick(view: T, throttle: Long = 250L, block: (T) -> Unit) { fun <T : View> LifecycleOwner.onClick(view: T, throttle: Long = 250L, block: (T) -> Unit) {
view.clicks().debounce(throttle).onEach { view.clicks().debounce(throttle).onEach {
block(view) block(view)
}.launchIn(this.lifecycleScope) }.launchIn(this.lifecycleScope)
} }

View File

@ -9,4 +9,4 @@ fun CharSequence.toColoredSpan(colorResId: Int, coloredPortion: String): CharSeq
val start = this@toColoredSpan.indexOf(coloredPortion) val start = this@toColoredSpan.indexOf(coloredPortion)
setSpan(ForegroundColorSpan(colorResId.toAppColor()), start, start + coloredPortion.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) setSpan(ForegroundColorSpan(colorResId.toAppColor()), start, start + coloredPortion.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
} }
} }

View File

@ -1,7 +1,9 @@
package cash.z.ecc.android.ext package cash.z.ecc.android.ext
import android.view.View import android.view.View
import android.view.View.* import android.view.View.GONE
import android.view.View.INVISIBLE
import android.view.View.VISIBLE
import cash.z.ecc.android.ui.MainActivity import cash.z.ecc.android.ui.MainActivity
import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.channelFlow import kotlinx.coroutines.flow.channelFlow
@ -40,8 +42,10 @@ fun View.onClickNavTo(navResId: Int, block: (() -> Any) = {}) {
setOnClickListener { setOnClickListener {
block() block()
(context as? MainActivity)?.safeNavigate(navResId) (context as? MainActivity)?.safeNavigate(navResId)
?: throw IllegalStateException("Cannot navigate from this activity. " + ?: throw IllegalStateException(
"Expected MainActivity but found ${context.javaClass.simpleName}") "Cannot navigate from this activity. " +
"Expected MainActivity but found ${context.javaClass.simpleName}"
)
} }
} }
@ -51,7 +55,7 @@ fun View.onClickNavUp(block: (() -> Any) = {}) {
(context as? MainActivity)?.navController?.navigateUp() (context as? MainActivity)?.navController?.navigateUp()
?: throw IllegalStateException( ?: throw IllegalStateException(
"Cannot navigate from this activity. " + "Cannot navigate from this activity. " +
"Expected MainActivity but found ${context.javaClass.simpleName}" "Expected MainActivity but found ${context.javaClass.simpleName}"
) )
} }
} }
@ -62,7 +66,7 @@ fun View.onClickNavBack(block: (() -> Any) = {}) {
(context as? MainActivity)?.navController?.popBackStack() (context as? MainActivity)?.navController?.popBackStack()
?: throw IllegalStateException( ?: throw IllegalStateException(
"Cannot navigate from this activity. " + "Cannot navigate from this activity. " +
"Expected MainActivity but found ${context.javaClass.simpleName}" "Expected MainActivity but found ${context.javaClass.simpleName}"
) )
} }
} }
@ -74,4 +78,4 @@ fun View.clicks() = channelFlow<View> {
awaitClose { awaitClose {
setOnClickListener(null) setOnClickListener(null)
} }
} }

View File

@ -58,7 +58,7 @@ class FeedbackBugsnag : FeedbackCoordinator.FeedbackObserver {
private class ReorgException(errorHeight: Int, rewindHeight: Int, reorgMesssage: String) : private class ReorgException(errorHeight: Int, rewindHeight: Int, reorgMesssage: String) :
Throwable(reorgMesssage) Throwable(reorgMesssage)
private class SendException(errorCode: Int?, errorMessage: String?): RuntimeException( private class SendException(errorCode: Int?, errorMessage: String?) : RuntimeException(
"Non-fatal error while sending transaction. code: $errorCode message: $errorMessage" "Non-fatal error while sending transaction. code: $errorCode message: $errorMessage"
) )
} }

View File

@ -19,4 +19,4 @@ class FeedbackConsole : FeedbackCoordinator.FeedbackObserver {
private fun log(message: String) { private fun log(message: String) {
Log.d("@TWIG", message) Log.d("@TWIG", message)
} }
} }

View File

@ -34,4 +34,4 @@ class FeedbackFile(fileName: String = "user_log.txt") :
it.writeUtf8("${format.format(System.currentTimeMillis())}|\t$message\n") it.writeUtf8("${format.format(System.currentTimeMillis())}|\t$message\n")
} }
} }
} }

View File

@ -29,5 +29,4 @@ class FeedbackMixpanel : FeedbackCoordinator.FeedbackObserver {
private fun track(eventName: String, properties: Map<String, Any>) { private fun track(eventName: String, properties: Map<String, Any>) {
mixpanel.trackMap(eventName, properties) mixpanel.trackMap(eventName, properties)
} }
}
}

View File

@ -22,11 +22,13 @@ object Report {
// Errors // Errors
abstract class Error(stepName: String, step: Int, val errorCode: Int?, val errorMessage: String?, vararg properties: Pair<String, Any>) : Send("error.$stepName", step, "isError" to true, *properties) abstract class Error(stepName: String, step: Int, val errorCode: Int?, val errorMessage: String?, vararg properties: Pair<String, Any>) : Send("error.$stepName", step, "isError" to true, *properties)
object ErrorNotFound : Error("notfound", 51, null, "Key not found") object ErrorNotFound : Error("notfound", 51, null, "Key not found")
class ErrorEncoding(errorCode: Int? = null, errorMessage: String? = null) : Error("encode", 71, errorCode, errorMessage, class ErrorEncoding(errorCode: Int? = null, errorMessage: String? = null) : Error(
"encode", 71, errorCode, errorMessage,
"errorCode" to (errorCode ?: -1), "errorCode" to (errorCode ?: -1),
"errorMessage" to (errorMessage ?: "None") "errorMessage" to (errorMessage ?: "None")
) )
class ErrorSubmitting(errorCode: Int? = null, errorMessage: String? = null) : Error("submit", 81, errorCode, errorMessage, class ErrorSubmitting(errorCode: Int? = null, errorMessage: String? = null) : Error(
"submit", 81, errorCode, errorMessage,
"errorCode" to (errorCode ?: -1), "errorCode" to (errorCode ?: -1),
"errorMessage" to (errorMessage ?: "None") "errorMessage" to (errorMessage ?: "None")
) )
@ -82,7 +84,7 @@ object Report {
*properties *properties
) { ) {
override val key = "performance.$name" override val key = "performance.$name"
override fun toString() = "$key: ${toMap().let { if(it.size > 1) "${it.entries}" else "" }}" override fun toString() = "$key: ${toMap().let { if (it.size > 1) "${it.entries}" else "" }}"
class ScanRate(network: String, cumulativeItems: Int, cumulativeTime: Long, cumulativeIps: Float) : Performance("scan.bps", "network" to network, "totalBlocks" to cumulativeItems, "totalTime" to cumulativeTime, "blocksPerSecond" to cumulativeIps) class ScanRate(network: String, cumulativeItems: Int, cumulativeTime: Long, cumulativeIps: Float) : Performance("scan.bps", "network" to network, "totalBlocks" to cumulativeItems, "totalTime" to cumulativeTime, "blocksPerSecond" to cumulativeIps)
} }
@ -94,7 +96,7 @@ object Report {
*properties *properties
) { ) {
override val key = "issue.$name" override val key = "issue.$name"
override fun toString() = "occurrence of ${key.replace('.', ' ')}${toMap().let { if(it.size > 1) " with ${it.entries}" else "" }}" override fun toString() = "occurrence of ${key.replace('.', ' ')}${toMap().let { if (it.size > 1) " with ${it.entries}" else "" }}"
// Issues with sending worth monitoring // Issues with sending worth monitoring
object SelfSend : Issue("self.send") object SelfSend : Issue("self.send")
@ -241,6 +243,5 @@ class LaunchMetric private constructor(private val metric: Feedback.TimeMetric)
override fun toString(): String = metric.toString() override fun toString(): String = metric.toString()
} }
inline fun <T> Feedback.measure(type: Report.MetricType, block: () -> T): T = inline fun <T> Feedback.measure(type: Report.MetricType, block: () -> T): T =
this.measure(type.key, type.description, block) this.measure(type.key, type.description, block)

View File

@ -85,7 +85,6 @@ import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import javax.inject.Inject import javax.inject.Inject
class MainActivity : AppCompatActivity() { class MainActivity : AppCompatActivity() {
@Inject @Inject
@ -224,9 +223,9 @@ class MainActivity : AppCompatActivity() {
} catch (t: Throwable) { } catch (t: Throwable) {
twig( twig(
"WARNING: during callback, did not navigate to destination: R.id.${ "WARNING: during callback, did not navigate to destination: R.id.${
resources.getResourceEntryName( resources.getResourceEntryName(
destination destination
) )
} due to: $t" } due to: $t"
) )
} }
@ -237,9 +236,9 @@ class MainActivity : AppCompatActivity() {
} catch (t: Throwable) { } catch (t: Throwable) {
twig( twig(
"WARNING: did not immediately navigate to destination: R.id.${ "WARNING: did not immediately navigate to destination: R.id.${
resources.getResourceEntryName( resources.getResourceEntryName(
destination destination
) )
} due to: $t" } due to: $t"
) )
} }
@ -310,7 +309,7 @@ class MainActivity : AppCompatActivity() {
} }
fun authenticate(description: String, title: String = getString(R.string.biometric_prompt_title), block: () -> Unit) { fun authenticate(description: String, title: String = getString(R.string.biometric_prompt_title), block: () -> Unit) {
val callback = object : BiometricPrompt.AuthenticationCallback() { val callback = object : BiometricPrompt.AuthenticationCallback() {
override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) { override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
twig("Authentication success with type: ${if (result.authenticationType == AUTHENTICATION_RESULT_TYPE_DEVICE_CREDENTIAL) "DEVICE_CREDENTIAL" else if (result.authenticationType == AUTHENTICATION_RESULT_TYPE_BIOMETRIC) "BIOMETRIC" else "UNKNOWN"} object: ${result.cryptoObject}") twig("Authentication success with type: ${if (result.authenticationType == AUTHENTICATION_RESULT_TYPE_DEVICE_CREDENTIAL) "DEVICE_CREDENTIAL" else if (result.authenticationType == AUTHENTICATION_RESULT_TYPE_BIOMETRIC) "BIOMETRIC" else "UNKNOWN"} object: ${result.cryptoObject}")
block() block()
@ -348,7 +347,7 @@ class MainActivity : AppCompatActivity() {
ERROR_TIMEOUT -> doNothing("Oops. It timed out.") ERROR_TIMEOUT -> doNothing("Oops. It timed out.")
ERROR_UNABLE_TO_PROCESS -> doNothing(".") ERROR_UNABLE_TO_PROCESS -> doNothing(".")
ERROR_VENDOR -> doNothing("We got some weird error and you should report this.") ERROR_VENDOR -> doNothing("We got some weird error and you should report this.")
else -> { else -> {
twig("Warning: unrecognized authentication error $errorCode") twig("Warning: unrecognized authentication error $errorCode")
doNothing("Authentication failed with error code $errorCode") doNothing("Authentication failed with error code $errorCode")
} }
@ -417,15 +416,18 @@ class MainActivity : AppCompatActivity() {
} }
fun preventBackPress(fragment: Fragment) { fun preventBackPress(fragment: Fragment) {
onFragmentBackPressed(fragment){} onFragmentBackPressed(fragment) {}
} }
fun onFragmentBackPressed(fragment: Fragment, block: () -> Unit) { fun onFragmentBackPressed(fragment: Fragment, block: () -> Unit) {
onBackPressedDispatcher.addCallback(fragment, object : OnBackPressedCallback(true) { onBackPressedDispatcher.addCallback(
override fun handleOnBackPressed() { fragment,
block() object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
block()
}
} }
}) )
} }
private fun showMessage(message: String, linger: Boolean = false) { private fun showMessage(message: String, linger: Boolean = false) {
@ -440,26 +442,26 @@ class MainActivity : AppCompatActivity() {
.make(view, "$message", Snackbar.LENGTH_INDEFINITE) .make(view, "$message", Snackbar.LENGTH_INDEFINITE)
.setAction(action) { /*auto-close*/ } .setAction(action) { /*auto-close*/ }
val snackBarView = snacks.view as ViewGroup val snackBarView = snacks.view as ViewGroup
val navigationBarHeight = resources.getDimensionPixelSize( val navigationBarHeight = resources.getDimensionPixelSize(
resources.getIdentifier( resources.getIdentifier(
"navigation_bar_height", "navigation_bar_height",
"dimen", "dimen",
"android" "android"
)
)
val params = snackBarView.getChildAt(0).layoutParams as ViewGroup.MarginLayoutParams
params.setMargins(
params.leftMargin,
params.topMargin,
params.rightMargin,
navigationBarHeight
) )
)
val params = snackBarView.getChildAt(0).layoutParams as ViewGroup.MarginLayoutParams
params.setMargins(
params.leftMargin,
params.topMargin,
params.rightMargin,
navigationBarHeight
)
snackBarView.getChildAt(0).setLayoutParams(params) snackBarView.getChildAt(0).setLayoutParams(params)
snacks snacks
} else { } else {
snackbar!!.setText(message).setAction(action) {/*auto-close*/} snackbar!!.setText(message).setAction(action) { /*auto-close*/ }
}.also { }.also {
if (!it.isShownOrQueued) it.show() if (!it.isShownOrQueued) it.show()
} }
@ -534,7 +536,8 @@ class MainActivity : AppCompatActivity() {
if (dialog == null && !ignoreScanFailure) throttle("scanFailure", 20_000L) { if (dialog == null && !ignoreScanFailure) throttle("scanFailure", 20_000L) {
notified = true notified = true
runOnUiThread { runOnUiThread {
dialog = showScanFailure(error, dialog = showScanFailure(
error,
onCancel = { dialog = null }, onCancel = { dialog = null },
onDismiss = { dialog = null } onDismiss = { dialog = null }
) )
@ -564,7 +567,6 @@ class MainActivity : AppCompatActivity() {
feedback.report(Reorg(errorHeight, rewindHeight)) feedback.report(Reorg(errorHeight, rewindHeight))
} }
// TODO: maybe move this quick helper code somewhere general or throttle the dialogs differently (like with a flow and stream operators, instead) // TODO: maybe move this quick helper code somewhere general or throttle the dialogs differently (like with a flow and stream operators, instead)
private val throttles = mutableMapOf<String, () -> Any>() private val throttles = mutableMapOf<String, () -> Any>()
@ -579,21 +581,19 @@ class MainActivity : AppCompatActivity() {
// after doing the work, check back in later and if another request came in, throttle it, otherwise exit // after doing the work, check back in later and if another request came in, throttle it, otherwise exit
throttles[key] = noWork throttles[key] = noWork
findViewById<View>(android.R.id.content).postDelayed({ findViewById<View>(android.R.id.content).postDelayed(
throttles[key]?.let { pendingWork -> {
throttles.remove(key) throttles[key]?.let { pendingWork ->
if (pendingWork !== noWork) throttle(key, delay, pendingWork) throttles.remove(key)
} if (pendingWork !== noWork) throttle(key, delay, pendingWork)
}, delay) }
},
delay
)
} }
/* Memo functions that might possibly get moved to MemoUtils */ /* Memo functions that might possibly get moved to MemoUtils */
// private val addressRegex = """zs\d\w{65,}""".toRegex()
suspend fun getSender(transaction: ConfirmedTransaction?): String { suspend fun getSender(transaction: ConfirmedTransaction?): String {
if (transaction == null) return getString(R.string.unknown) if (transaction == null) return getString(R.string.unknown)
return MemoUtil.findAddressInMemo(transaction, ::isValidAddress)?.toAbbreviatedAddress() ?: getString(R.string.unknown) return MemoUtil.findAddressInMemo(transaction, ::isValidAddress)?.toAbbreviatedAddress() ?: getString(R.string.unknown)
@ -655,5 +655,4 @@ class MainActivity : AppCompatActivity() {
twig("Warning: failed to open browser due to $t") twig("Warning: failed to open browser due to $t")
} }
} }
} }

View File

@ -34,4 +34,4 @@ class MainViewModel @Inject constructor() : ViewModel() {
twig("MainViewModel.setSyncReady: $isReady") twig("MainViewModel.setSyncReady: $isReady")
_syncReady.value = isReady _syncReady.value = isReady
} }
} }

View File

@ -10,10 +10,15 @@ import androidx.lifecycle.lifecycleScope
import androidx.viewbinding.ViewBinding import androidx.viewbinding.ViewBinding
import cash.z.ecc.android.feedback.Report import cash.z.ecc.android.feedback.Report
import cash.z.ecc.android.ui.MainActivity import cash.z.ecc.android.ui.MainActivity
import kotlinx.coroutines.* import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
abstract class BaseFragment<T : ViewBinding> : Fragment() { abstract class BaseFragment<T : ViewBinding> : Fragment() {
val mainActivity: MainActivity? get() = activity as MainActivity? val mainActivity: MainActivity? get() = activity as MainActivity?
@ -79,4 +84,4 @@ abstract class BaseFragment<T : ViewBinding> : Fragment() {
} }
} }
} }
} }

View File

@ -10,7 +10,12 @@ import androidx.recyclerview.widget.RecyclerView
import cash.z.ecc.android.R import cash.z.ecc.android.R
import cash.z.ecc.android.databinding.FragmentHistoryBinding import cash.z.ecc.android.databinding.FragmentHistoryBinding
import cash.z.ecc.android.di.viewmodel.activityViewModel import cash.z.ecc.android.di.viewmodel.activityViewModel
import cash.z.ecc.android.ext.* import cash.z.ecc.android.ext.WalletZecFormmatter
import cash.z.ecc.android.ext.goneIf
import cash.z.ecc.android.ext.onClickNavUp
import cash.z.ecc.android.ext.pending
import cash.z.ecc.android.ext.toAppString
import cash.z.ecc.android.ext.toColoredSpan
import cash.z.ecc.android.feedback.Report import cash.z.ecc.android.feedback.Report
import cash.z.ecc.android.feedback.Report.Tap.HISTORY_BACK import cash.z.ecc.android.feedback.Report.Tap.HISTORY_BACK
import cash.z.ecc.android.sdk.db.entity.ConfirmedTransaction import cash.z.ecc.android.sdk.db.entity.ConfirmedTransaction
@ -21,7 +26,6 @@ import cash.z.ecc.android.sdk.type.WalletBalance
import cash.z.ecc.android.ui.base.BaseFragment import cash.z.ecc.android.ui.base.BaseFragment
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
class HistoryFragment : BaseFragment<FragmentHistoryBinding>() { class HistoryFragment : BaseFragment<FragmentHistoryBinding>() {
override val screen = Report.Screen.HISTORY override val screen = Report.Screen.HISTORY
@ -34,7 +38,6 @@ class HistoryFragment : BaseFragment<FragmentHistoryBinding>() {
override fun inflate(inflater: LayoutInflater): FragmentHistoryBinding = override fun inflate(inflater: LayoutInflater): FragmentHistoryBinding =
FragmentHistoryBinding.inflate(inflater) FragmentHistoryBinding.inflate(inflater)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
twig("HistoryFragment.onViewCreated") twig("HistoryFragment.onViewCreated")
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
@ -61,7 +64,7 @@ class HistoryFragment : BaseFragment<FragmentHistoryBinding>() {
goneIf(change <= 0L) goneIf(change <= 0L)
val changeString = WalletZecFormmatter.toZecStringFull(change) val changeString = WalletZecFormmatter.toZecStringFull(change)
val expecting = R.string.home_banner_expecting.toAppString(true) val expecting = R.string.home_banner_expecting.toAppString(true)
text = "($expecting +$changeString ZEC)".toColoredSpan(R.color.text_light, "+${changeString}") text = "($expecting +$changeString ZEC)".toColoredSpan(R.color.text_light, "+$changeString")
} }
} }
@ -90,9 +93,12 @@ class HistoryFragment : BaseFragment<FragmentHistoryBinding>() {
private fun scrollToTop() { private fun scrollToTop() {
twig("scrolling to the top") twig("scrolling to the top")
binding.recyclerTransactions.apply { binding.recyclerTransactions.apply {
postDelayed({ postDelayed(
smoothScrollToPosition(0) {
}, 5L) smoothScrollToPosition(0)
},
5L
)
} }
} }
@ -100,4 +106,4 @@ class HistoryFragment : BaseFragment<FragmentHistoryBinding>() {
fun onLastItemShown(item: ConfirmedTransaction, position: Int) { fun onLastItemShown(item: ConfirmedTransaction, position: Int) {
binding.footerFade.alpha = position.toFloat() / (binding.recyclerTransactions.adapter?.itemCount ?: 1) binding.footerFade.alpha = position.toFloat() / (binding.recyclerTransactions.adapter?.itemCount ?: 1)
} }
} }

View File

@ -68,7 +68,6 @@ class HistoryViewModel @Inject constructor() : ViewModel() {
var txId: String? = null var txId: String? = null
) )
private suspend fun ConfirmedTransaction?.toUiModel(latestHeight: Int? = null): UiModel = UiModel().apply { private suspend fun ConfirmedTransaction?.toUiModel(latestHeight: Int? = null): UiModel = UiModel().apply {
this@toUiModel.let { tx -> this@toUiModel.let { tx ->
txId = toTxId(tx?.rawTransactionId) txId = toTxId(tx?.rawTransactionId)
@ -105,7 +104,8 @@ class HistoryViewModel @Inject constructor() : ViewModel() {
if (it.minedHeight > 0 && hasLatestHeight) { if (it.minedHeight > 0 && hasLatestHeight) {
val confirmations = latestHeight!! - it.minedHeight + 1 val confirmations = latestHeight!! - it.minedHeight + 1
confirmation = if (confirmations >= 10) getString(R.string.transaction_status_confirmed) else "$confirmations ${getString( confirmation = if (confirmations >= 10) getString(R.string.transaction_status_confirmed) else "$confirmations ${getString(
R.string.transaction_status_confirming)}" R.string.transaction_status_confirming
)}"
} else { } else {
if (!hasLatestHeight && isSufficientlyOld(tx)) { if (!hasLatestHeight && isSufficientlyOld(tx)) {
twig("Warning: could not load latestheight from server to determine confirmations but this transaction is mined and old enough to be considered confirmed") twig("Warning: could not load latestheight from server to determine confirmations but this transaction is mined and old enough to be considered confirmed")
@ -118,11 +118,8 @@ class HistoryViewModel @Inject constructor() : ViewModel() {
} else { } else {
confirmation = getString(R.string.transaction_status_pending) confirmation = getString(R.string.transaction_status_pending)
} }
} }
// val mainActivity = (context as MainActivity)
// inbound v. outbound values
when (isInbound) { when (isInbound) {
true -> { true -> {
topLabel = getString(R.string.transaction_story_inbound) topLabel = getString(R.string.transaction_story_inbound)
@ -154,7 +151,7 @@ class HistoryViewModel @Inject constructor() : ViewModel() {
private fun toTxId(tx: ByteArray?): String? { private fun toTxId(tx: ByteArray?): String? {
if (tx == null) return null if (tx == null) return null
val sb = StringBuilder(tx.size * 2) val sb = StringBuilder(tx.size * 2)
for(i in (tx.size - 1) downTo 0) { for (i in (tx.size - 1) downTo 0) {
sb.append(String.format("%02x", tx[i])) sb.append(String.format("%02x", tx[i]))
} }
return sb.toString() return sb.toString()
@ -164,10 +161,9 @@ class HistoryViewModel @Inject constructor() : ViewModel() {
// the goal is just to improve the edge cases where the latest height isn't known but other // the goal is just to improve the edge cases where the latest height isn't known but other
// information suggests that the TX is confirmed. We can improve this, later. // information suggests that the TX is confirmed. We can improve this, later.
private fun isSufficientlyOld(tx: ConfirmedTransaction): Boolean { private fun isSufficientlyOld(tx: ConfirmedTransaction): Boolean {
val threshold = 75 * 1000 * 25 //approx 25 blocks val threshold = 75 * 1000 * 25 // approx 25 blocks
val delta = System.currentTimeMillis() / 1000L - tx.blockTimeInSeconds val delta = System.currentTimeMillis() / 1000L - tx.blockTimeInSeconds
return tx.minedHeight > synchronizer.network.saplingActivationHeight return tx.minedHeight > synchronizer.network.saplingActivationHeight &&
&& delta < threshold delta < threshold
} }
} }

View File

@ -13,9 +13,9 @@ class TransactionAdapter<T : ConfirmedTransaction> :
override fun areItemsTheSame( override fun areItemsTheSame(
oldItem: T, oldItem: T,
newItem: T newItem: T
) = oldItem.minedHeight == newItem.minedHeight && oldItem.noteId == newItem.noteId ) = oldItem.minedHeight == newItem.minedHeight && oldItem.noteId == newItem.noteId &&
// bugfix: distinguish between self-transactions so they don't overwrite each other in the UI // TODO confirm that this is working, as intended // bugfix: distinguish between self-transactions so they don't overwrite each other in the UI // TODO confirm that this is working, as intended
&& ((oldItem.raw == null && newItem.raw == null) || (oldItem.raw != null && newItem.raw != null && oldItem.raw!!.contentEquals(newItem.raw!!))) ((oldItem.raw == null && newItem.raw == null) || (oldItem.raw != null && newItem.raw != null && oldItem.raw!!.contentEquals(newItem.raw!!)))
override fun areContentsTheSame( override fun areContentsTheSame(
oldItem: T, oldItem: T,

View File

@ -4,32 +4,34 @@ import android.content.res.ColorStateList
import android.graphics.ColorMatrix import android.graphics.ColorMatrix
import android.graphics.ColorMatrixColorFilter import android.graphics.ColorMatrixColorFilter
import android.os.Bundle import android.os.Bundle
import android.text.format.DateUtils
import android.text.method.ScrollingMovementMethod import android.text.method.ScrollingMovementMethod
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import androidx.core.view.ViewCompat import androidx.core.view.ViewCompat
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.transition.* import androidx.transition.ChangeBounds
import androidx.transition.ChangeClipBounds
import androidx.transition.ChangeTransform
import androidx.transition.Transition
import androidx.transition.TransitionSet
import cash.z.ecc.android.R import cash.z.ecc.android.R
import cash.z.ecc.android.ZcashWalletApp
import cash.z.ecc.android.databinding.FragmentTransactionBinding import cash.z.ecc.android.databinding.FragmentTransactionBinding
import cash.z.ecc.android.di.viewmodel.activityViewModel import cash.z.ecc.android.di.viewmodel.activityViewModel
import cash.z.ecc.android.ext.* import cash.z.ecc.android.ext.Const
import cash.z.ecc.android.ext.gone
import cash.z.ecc.android.ext.invisible
import cash.z.ecc.android.ext.onClickNavBack
import cash.z.ecc.android.ext.toAppColor
import cash.z.ecc.android.ext.toColoredSpan
import cash.z.ecc.android.ext.visible
import cash.z.ecc.android.feedback.Report import cash.z.ecc.android.feedback.Report
import cash.z.ecc.android.sdk.db.entity.ConfirmedTransaction
import cash.z.ecc.android.sdk.ext.ZcashSdk
import cash.z.ecc.android.sdk.ext.toAbbreviatedAddress import cash.z.ecc.android.sdk.ext.toAbbreviatedAddress
import cash.z.ecc.android.sdk.ext.twig import cash.z.ecc.android.sdk.ext.twig
import cash.z.ecc.android.ui.MainActivity
import cash.z.ecc.android.ui.base.BaseFragment import cash.z.ecc.android.ui.base.BaseFragment
import cash.z.ecc.android.ui.history.HistoryViewModel.UiModel import cash.z.ecc.android.ui.history.HistoryViewModel.UiModel
import cash.z.ecc.android.ui.util.toUtf8Memo
import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import java.util.*
class TransactionFragment : BaseFragment<FragmentTransactionBinding>() { class TransactionFragment : BaseFragment<FragmentTransactionBinding>() {
override val screen = Report.Screen.TRANSACTION override val screen = Report.Screen.TRANSACTION
@ -53,7 +55,7 @@ class TransactionFragment : BaseFragment<FragmentTransactionBinding>() {
// sharedElementReturnTransition = ChangeBounds().apply { duration = 1500 } // sharedElementReturnTransition = ChangeBounds().apply { duration = 1500 }
// enterTransition = Fade().apply { // enterTransition = Fade().apply {
// duration = 1800 // duration = 1800
//// slideEdge = Gravity.END // // slideEdge = Gravity.END
// } // }
} }
@ -117,7 +119,6 @@ class TransactionFragment : BaseFragment<FragmentTransactionBinding>() {
uiModel.toAddressLabel()?.let { subwaySpotAddress.visible(); subwayLabelAddress.visible(); subwayLabelAddress.text = it } uiModel.toAddressLabel()?.let { subwaySpotAddress.visible(); subwayLabelAddress.visible(); subwayLabelAddress.text = it }
uiModel.toAddressClickListener()?.let { subwayLabelAddress.setOnClickListener(it) } uiModel.toAddressClickListener()?.let { subwayLabelAddress.setOnClickListener(it) }
// TODO: remove logic from sections below and add more fields or extension functions to UiModel // TODO: remove logic from sections below and add more fields or extension functions to UiModel
uiModel.confirmation?.let { uiModel.confirmation?.let {
subwaySpotConfirmations.visible(); subwayLabelConfirmations.visible() subwaySpotConfirmations.visible(); subwayLabelConfirmations.visible()
@ -162,7 +163,7 @@ class TransactionFragment : BaseFragment<FragmentTransactionBinding>() {
binding.subwaySpotMemoContent.rotation = 90.0f binding.subwaySpotMemoContent.rotation = 90.0f
} else { } else {
binding.subwayLabelMemo.setText(getString(R.string.transaction_with_memo)) binding.subwayLabelMemo.setText(getString(R.string.transaction_with_memo))
binding.subwayLabelMemo.scrollTo(0,0) binding.subwayLabelMemo.scrollTo(0, 0)
binding.subwayLabelMemo.invalidate() binding.subwayLabelMemo.invalidate()
twig("setting memo text to: with a memo") twig("setting memo text to: with a memo")
binding.groupMemoIcon.visible() binding.groupMemoIcon.visible()
@ -196,12 +197,4 @@ class TransactionFragment : BaseFragment<FragmentTransactionBinding>() {
it.toColoredSpan(R.color.tx_text_light_dimmed, if (address == null) it else prefix) it.toColoredSpan(R.color.tx_text_light_dimmed, if (address == null) it else prefix)
} }
} }
} }

View File

@ -4,10 +4,7 @@ import android.graphics.drawable.Drawable
import android.view.View import android.view.View
import android.widget.ImageView import android.widget.ImageView
import android.widget.TextView import android.widget.TextView
import androidx.annotation.ColorRes
import androidx.annotation.IntegerRes
import androidx.annotation.StringRes import androidx.annotation.StringRes
import androidx.appcompat.content.res.AppCompatResources
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import cash.z.ecc.android.R import cash.z.ecc.android.R
@ -22,12 +19,12 @@ import cash.z.ecc.android.sdk.ext.ZcashSdk
import cash.z.ecc.android.sdk.ext.isShielded import cash.z.ecc.android.sdk.ext.isShielded
import cash.z.ecc.android.sdk.ext.toAbbreviatedAddress import cash.z.ecc.android.sdk.ext.toAbbreviatedAddress
import cash.z.ecc.android.ui.MainActivity import cash.z.ecc.android.ui.MainActivity
import cash.z.ecc.android.ui.util.MemoUtil
import cash.z.ecc.android.ui.util.toUtf8Memo import cash.z.ecc.android.ui.util.toUtf8Memo
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
class TransactionViewHolder<T : ConfirmedTransaction>(itemView: View) : RecyclerView.ViewHolder(itemView) { class TransactionViewHolder<T : ConfirmedTransaction>(itemView: View) : RecyclerView.ViewHolder(itemView) {
private val indicator = itemView.findViewById<View>(R.id.indicator) private val indicator = itemView.findViewById<View>(R.id.indicator)
private val amountText = itemView.findViewById<TextView>(R.id.text_transaction_amount) private val amountText = itemView.findViewById<TextView>(R.id.text_transaction_amount)
private val topText = itemView.findViewById<TextView>(R.id.text_transaction_top) private val topText = itemView.findViewById<TextView>(R.id.text_transaction_top)
@ -40,7 +37,7 @@ class TransactionViewHolder<T : ConfirmedTransaction>(itemView: View) : Recycler
val mainActivity = itemView.context as MainActivity val mainActivity = itemView.context as MainActivity
mainActivity.lifecycleScope.launch { mainActivity.lifecycleScope.launch {
// update view // update view
var lineOne:CharSequence = "" var lineOne: CharSequence = ""
var lineTwo = "" var lineTwo = ""
var amountZec = "" var amountZec = ""
var amountDisplay = "" var amountDisplay = ""
@ -70,7 +67,7 @@ class TransactionViewHolder<T : ConfirmedTransaction>(itemView: View) : Recycler
lineOne = "${if (isMined) str(R.string.transaction_address_you_paid) else str(R.string.transaction_address_paying)} ${toAddress?.toAbbreviatedAddress()}" lineOne = "${if (isMined) str(R.string.transaction_address_you_paid) else str(R.string.transaction_address_paying)} ${toAddress?.toAbbreviatedAddress()}"
lineTwo = if (isMined) "${str(R.string.transaction_status_sent)} $timestamp" else str(R.string.transaction_status_pending) lineTwo = if (isMined) "${str(R.string.transaction_status_sent)} $timestamp" else str(R.string.transaction_status_pending)
// TODO: this logic works but is sloppy. Find a more robust solution to displaying information about expiration (such as expires in 1 block, etc). Then if it is way beyond expired, remove it entirely. Perhaps give the user a button for that (swipe to dismiss?) // TODO: this logic works but is sloppy. Find a more robust solution to displaying information about expiration (such as expires in 1 block, etc). Then if it is way beyond expired, remove it entirely. Perhaps give the user a button for that (swipe to dismiss?)
if(!isMined && (expiryHeight != null) && (expiryHeight!! < mainActivity.latestHeight ?: -1)) lineTwo = str(R.string.transaction_status_expired) if (!isMined && (expiryHeight != null) && (expiryHeight!! < mainActivity.latestHeight ?: -1)) lineTwo = str(R.string.transaction_status_expired)
amountDisplay = "- $amountZec" amountDisplay = "- $amountZec"
if (isMined) { if (isMined) {
arrowRotation = R.integer.transaction_arrow_rotation_send arrowRotation = R.integer.transaction_arrow_rotation_send
@ -132,7 +129,8 @@ class TransactionViewHolder<T : ConfirmedTransaction>(itemView: View) : Recycler
indicator.setBackgroundColor(indicatorBackground.toAppColor()) indicator.setBackgroundColor(indicatorBackground.toAppColor())
transactionArrow.setColorFilter(arrowBackgroundTint.toAppColor()) transactionArrow.setColorFilter(arrowBackgroundTint.toAppColor())
transactionArrow.rotation = arrowRotation.toAppInt().toFloat() transactionArrow.rotation = arrowRotation.toAppInt().toFloat()
var bottomTextRightDrawable:Drawable? = null
var bottomTextRightDrawable: Drawable? = null
iconMemo.goneIf(!transaction?.memo.toUtf8Memo().isNotEmpty()) iconMemo.goneIf(!transaction?.memo.toUtf8Memo().isNotEmpty())
bottomText.setCompoundDrawablesWithIntrinsicBounds(null, null, bottomTextRightDrawable, null) bottomText.setCompoundDrawablesWithIntrinsicBounds(null, null, bottomTextRightDrawable, null)
} }
@ -153,7 +151,4 @@ class TransactionViewHolder<T : ConfirmedTransaction>(itemView: View) : Recycler
} }
private inline fun str(@StringRes resourceId: Int) = itemView.context.getString(resourceId) private inline fun str(@StringRes resourceId: Int) = itemView.context.getString(resourceId)
} }

View File

@ -1,15 +1,15 @@
package cash.z.ecc.android.ui.history package cash.z.ecc.android.ui.history
// //
//import android.content.Context // import android.content.Context
//import android.graphics.Canvas // import android.graphics.Canvas
//import android.graphics.Rect // import android.graphics.Rect
//import android.view.LayoutInflater // import android.view.LayoutInflater
//import android.view.View // import android.view.View
//import androidx.recyclerview.widget.RecyclerView // import androidx.recyclerview.widget.RecyclerView
//import cash.z.ecc.android.R // import cash.z.ecc.android.R
// //
// //
//class TransactionsDrawableFooter(context: Context) : RecyclerView.ItemDecoration() { // class TransactionsDrawableFooter(context: Context) : RecyclerView.ItemDecoration() {
// //
// private var footer: View = // private var footer: View =
// LayoutInflater.from(context).inflate(R.layout.footer_transactions, null, false) // LayoutInflater.from(context).inflate(R.layout.footer_transactions, null, false)
@ -49,4 +49,4 @@ package cash.z.ecc.android.ui.history
// outRect.setEmpty() // outRect.setEmpty()
// } // }
// } // }
//} // }

View File

@ -8,7 +8,6 @@ import android.view.View
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import cash.z.ecc.android.R import cash.z.ecc.android.R
class TransactionsFooter(context: Context) : RecyclerView.ItemDecoration() { class TransactionsFooter(context: Context) : RecyclerView.ItemDecoration() {
private var footer: Drawable = context.resources.getDrawable(R.drawable.background_footer) private var footer: Drawable = context.resources.getDrawable(R.drawable.background_footer)

View File

@ -68,7 +68,6 @@ class HomeFragment : BaseFragment<FragmentHomeBinding>() {
override fun inflate(inflater: LayoutInflater): FragmentHomeBinding = override fun inflate(inflater: LayoutInflater): FragmentHomeBinding =
FragmentHomeBinding.inflate(inflater) FragmentHomeBinding.inflate(inflater)
// //
// LifeCycle // LifeCycle
// //
@ -144,7 +143,6 @@ class HomeFragment : BaseFragment<FragmentHomeBinding>() {
// if the model already existed, cool but let the sendViewModel be the source of truth for the amount // if the model already existed, cool but let the sendViewModel be the source of truth for the amount
onModelUpdated(null, uiModel.copy(pendingSend = WalletZecFormmatter.toZecStringFull(sendViewModel.zatoshiAmount.coerceAtLeast(0)))) onModelUpdated(null, uiModel.copy(pendingSend = WalletZecFormmatter.toZecStringFull(sendViewModel.zatoshiAmount.coerceAtLeast(0))))
} }
} }
private fun onClearAmount() { private fun onClearAmount() {
@ -190,7 +188,6 @@ class HomeFragment : BaseFragment<FragmentHomeBinding>() {
} }
} }
// //
// Public UI API // Public UI API
// //
@ -277,7 +274,7 @@ class HomeFragment : BaseFragment<FragmentHomeBinding>() {
val change = WalletZecFormmatter.toZecStringFull(totalBalance - availableBalance) val change = WalletZecFormmatter.toZecStringFull(totalBalance - availableBalance)
"(${getString(R.string.home_banner_expecting)} +$change ZEC)".toColoredSpan(R.color.text_light, "+$change") "(${getString(R.string.home_banner_expecting)} +$change ZEC)".toColoredSpan(R.color.text_light, "+$change")
} else { } else {
getString(R.string.home_instruction_enter_amount) getString(R.string.home_instruction_enter_amount)
} }
} }
} }
@ -294,7 +291,6 @@ class HomeFragment : BaseFragment<FragmentHomeBinding>() {
} }
} }
// //
// Private UI Events // Private UI Events
// //
@ -320,9 +316,9 @@ class HomeFragment : BaseFragment<FragmentHomeBinding>() {
else -> { else -> {
buildString { buildString {
append("UiModel(") append("UiModel(")
if (old.status != new.status) append ("status=${new.status}") if (old.status != new.status) append("status=${new.status}")
if (old.processorInfo != new.processorInfo) { if (old.processorInfo != new.processorInfo) {
append ("${maybeComma()}processorInfo=ProcessorInfo(") append("${maybeComma()}processorInfo=ProcessorInfo(")
val startLength = length val startLength = length
fun innerComma() = if (length > startLength) ", " else "" fun innerComma() = if (length > startLength) ", " else ""
if (old.processorInfo.networkBlockHeight != new.processorInfo.networkBlockHeight) append("networkBlockHeight=${new.processorInfo.networkBlockHeight}") if (old.processorInfo.networkBlockHeight != new.processorInfo.networkBlockHeight) append("networkBlockHeight=${new.processorInfo.networkBlockHeight}")
@ -332,9 +328,9 @@ class HomeFragment : BaseFragment<FragmentHomeBinding>() {
if (old.processorInfo.lastScanRange != new.processorInfo.lastScanRange) append("${innerComma()}lastScanRange=${new.processorInfo.lastScanRange}") if (old.processorInfo.lastScanRange != new.processorInfo.lastScanRange) append("${innerComma()}lastScanRange=${new.processorInfo.lastScanRange}")
append(")") append(")")
} }
if (old.availableBalance != new.availableBalance) append ("${maybeComma()}availableBalance=${new.availableBalance}") if (old.availableBalance != new.availableBalance) append("${maybeComma()}availableBalance=${new.availableBalance}")
if (old.totalBalance != new.totalBalance) append ("${maybeComma()}totalBalance=${new.totalBalance}") if (old.totalBalance != new.totalBalance) append("${maybeComma()}totalBalance=${new.totalBalance}")
if (old.pendingSend != new.pendingSend) append ("${maybeComma()}pendingSend=${new.pendingSend}") if (old.pendingSend != new.pendingSend) append("${maybeComma()}pendingSend=${new.pendingSend}")
append(")") append(")")
} }
} }
@ -413,7 +409,6 @@ class HomeFragment : BaseFragment<FragmentHomeBinding>() {
}.launchIn(resumedScope) }.launchIn(resumedScope)
} }
// //
// Inner classes and extensions // Inner classes and extensions
// //
@ -444,11 +439,10 @@ class HomeFragment : BaseFragment<FragmentHomeBinding>() {
return this return this
} }
// //
// User Interruptions // User Interruptions
// //
//TODO: Expand this placeholder logic around when to interrupt the user. // TODO: Expand this placeholder logic around when to interrupt the user.
// For now, we just need to get this in the app so that we can BEGIN capturing ECC feedback. // For now, we just need to get this in the app so that we can BEGIN capturing ECC feedback.
var hasInterrupted = false var hasInterrupted = false
private fun canInterruptUser(): Boolean { private fun canInterruptUser(): Boolean {
@ -536,4 +530,4 @@ class HomeFragment : BaseFragment<FragmentHomeBinding>() {
override fun onDetach() { override fun onDetach() {
super.onDetach() super.onDetach()
} }
} }

View File

@ -5,14 +5,23 @@ import cash.z.ecc.android.R
import cash.z.ecc.android.ext.toAppString import cash.z.ecc.android.ext.toAppString
import cash.z.ecc.android.sdk.SdkSynchronizer import cash.z.ecc.android.sdk.SdkSynchronizer
import cash.z.ecc.android.sdk.Synchronizer import cash.z.ecc.android.sdk.Synchronizer
import cash.z.ecc.android.sdk.Synchronizer.Status.* import cash.z.ecc.android.sdk.Synchronizer.Status.DISCONNECTED
import cash.z.ecc.android.sdk.Synchronizer.Status.DOWNLOADING
import cash.z.ecc.android.sdk.Synchronizer.Status.SCANNING
import cash.z.ecc.android.sdk.Synchronizer.Status.SYNCED
import cash.z.ecc.android.sdk.Synchronizer.Status.VALIDATING
import cash.z.ecc.android.sdk.block.CompactBlockProcessor import cash.z.ecc.android.sdk.block.CompactBlockProcessor
import cash.z.ecc.android.sdk.exception.RustLayerException import cash.z.ecc.android.sdk.exception.RustLayerException
import cash.z.ecc.android.sdk.ext.ZcashSdk.MINERS_FEE_ZATOSHI import cash.z.ecc.android.sdk.ext.ZcashSdk.MINERS_FEE_ZATOSHI
import cash.z.ecc.android.sdk.ext.ZcashSdk.ZATOSHI_PER_ZEC import cash.z.ecc.android.sdk.ext.ZcashSdk.ZATOSHI_PER_ZEC
import cash.z.ecc.android.sdk.ext.twig import cash.z.ecc.android.sdk.ext.twig
import kotlinx.coroutines.channels.ConflatedBroadcastChannel import kotlinx.coroutines.channels.ConflatedBroadcastChannel
import kotlinx.coroutines.flow.* import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.asFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.conflate
import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.flow.scan
import javax.inject.Inject import javax.inject.Inject
import kotlin.math.roundToInt import kotlin.math.roundToInt
@ -39,14 +48,14 @@ class HomeViewModel @Inject constructor() : ViewModel() {
} }
_typedChars = ConflatedBroadcastChannel() _typedChars = ConflatedBroadcastChannel()
val typedChars = _typedChars.asFlow() val typedChars = _typedChars.asFlow()
val decimal = '.'// R.string.key_decimal.toAppString()[0] val decimal = '.' // R.string.key_decimal.toAppString()[0]
val backspace = R.string.key_backspace.toAppString()[0] val backspace = R.string.key_backspace.toAppString()[0]
val zec = typedChars.scan(preTypedChars) { acc, c -> val zec = typedChars.scan(preTypedChars) { acc, c ->
when { when {
// no-op cases // no-op cases
acc == "0" && c == '0' acc == "0" && c == '0' ||
|| (c == backspace && acc == "0") (c == backspace && acc == "0")
|| (c == decimal && acc.contains(decimal)) -> { || (c == decimal && acc.contains(decimal)) -> {
acc acc
} }
c == backspace && acc.length <= 1 -> { c == backspace && acc.length <= 1 -> {
@ -68,9 +77,9 @@ class HomeViewModel @Inject constructor() : ViewModel() {
} }
twig("initializing view models stream") twig("initializing view models stream")
uiModels = synchronizer.run { uiModels = synchronizer.run {
combine(status, processorInfo, balances, zec) { s, p, b, z-> combine(status, processorInfo, balances, zec) { s, p, b, z ->
UiModel(s, p, b.availableZatoshi, b.totalZatoshi, z) UiModel(s, p, b.availableZatoshi, b.totalZatoshi, z)
}.onStart{ emit(UiModel()) } }.onStart { emit(UiModel()) }
}.conflate() }.conflate()
} }

View File

@ -1,7 +1,6 @@
package cash.z.ecc.android.ui.home package cash.z.ecc.android.ui.home
import android.animation.ValueAnimator import android.animation.ValueAnimator
import cash.z.ecc.android.sdk.ext.twig
import com.airbnb.lottie.LottieAnimationView import com.airbnb.lottie.LottieAnimationView
class MagicSnakeLoader( class MagicSnakeLoader(
@ -54,14 +53,17 @@ class MagicSnakeLoader(
private fun startMaybe() { private fun startMaybe() {
if (!isSynced && !isStarted) lottie.postDelayed({ if (!isSynced && !isStarted) lottie.postDelayed(
// after some delay, if we're still not synced then we better start animating (unless we already are)! {
if (!isSynced && isPaused) { // after some delay, if we're still not synced then we better start animating (unless we already are)!
lottie.resumeAnimation() if (!isSynced && isPaused) {
isPaused = false lottie.resumeAnimation()
isStarted = true isPaused = false
} isStarted = true
}, 200L) }
},
200L
)
} }
private val isDownloading get() = downloadProgress in 1..99 private val isDownloading get() = downloadProgress in 1..99
@ -84,7 +86,7 @@ class MagicSnakeLoader(
} }
} }
private val acceptablePauseFrames = arrayOf(33,34,67,68,99) private val acceptablePauseFrames = arrayOf(33, 34, 67, 68, 99)
private fun applyScanProgress(frame: Int) { private fun applyScanProgress(frame: Int) {
// don't hardcode the progress until the loop animation has completed, cleanly // don't hardcode the progress until the loop animation has completed, cleanly
if (isPaused) { if (isPaused) {
@ -117,7 +119,7 @@ class MagicSnakeLoader(
} }
private fun removeLoops() { private fun removeLoops() {
lottie.frame.let {frame -> lottie.frame.let { frame ->
if (frame in 33..67) { if (frame in 33..67) {
lottie.frame = frame + 34 lottie.frame = frame + 34
} else if (frame in 0..33) { } else if (frame in 0..33) {
@ -151,4 +153,3 @@ class MagicSnakeLoader(
return ((animatedValue as Float) * totalFrames).toInt() return ((animatedValue as Float) * totalFrames).toInt()
} }
} }

View File

@ -19,7 +19,6 @@ import cash.z.ecc.android.feedback.Report
import cash.z.ecc.android.feedback.Report.Tap.AWESOME_CLOSE import cash.z.ecc.android.feedback.Report.Tap.AWESOME_CLOSE
import cash.z.ecc.android.feedback.Report.Tap.AWESOME_SHIELD import cash.z.ecc.android.feedback.Report.Tap.AWESOME_SHIELD
import cash.z.ecc.android.feedback.Report.Tap.COPY_TRANSPARENT_ADDRESS import cash.z.ecc.android.feedback.Report.Tap.COPY_TRANSPARENT_ADDRESS
import cash.z.ecc.android.sdk.SdkSynchronizer
import cash.z.ecc.android.sdk.db.entity.PendingTransaction import cash.z.ecc.android.sdk.db.entity.PendingTransaction
import cash.z.ecc.android.sdk.db.entity.isCancelled import cash.z.ecc.android.sdk.db.entity.isCancelled
import cash.z.ecc.android.sdk.db.entity.isCreated import cash.z.ecc.android.sdk.db.entity.isCreated
@ -37,7 +36,6 @@ import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
class AwesomeFragment : BaseFragment<FragmentAwesomeBinding>() { class AwesomeFragment : BaseFragment<FragmentAwesomeBinding>() {
override val screen = Report.Screen.AWESOME override val screen = Report.Screen.AWESOME
@ -96,7 +94,6 @@ class AwesomeFragment : BaseFragment<FragmentAwesomeBinding>() {
viewModel.getTransparentBalance().let { balance -> viewModel.getTransparentBalance().let { balance ->
onBalanceUpdated(balance, utxoCount) onBalanceUpdated(balance, utxoCount)
} }
} }
private fun onAddressLoaded(address: String) { private fun onAddressLoaded(address: String) {
@ -119,7 +116,7 @@ class AwesomeFragment : BaseFragment<FragmentAwesomeBinding>() {
1 -> binding.textAddressPart2 1 -> binding.textAddressPart2
else -> throw IllegalArgumentException( else -> throw IllegalArgumentException(
"Unexpected address index $index. Unable to split the t-addr into two parts." + "Unexpected address index $index. Unable to split the t-addr into two parts." +
" Ensure that the address is valid." " Ensure that the address is valid."
) )
} }
@ -200,7 +197,6 @@ class AwesomeFragment : BaseFragment<FragmentAwesomeBinding>() {
} }
} }
private fun onShieldComplete(isSuccess: Boolean) { private fun onShieldComplete(isSuccess: Boolean) {
binding.lottieShielding.visibility = View.GONE binding.lottieShielding.visibility = View.GONE
@ -248,9 +244,6 @@ class AwesomeFragment : BaseFragment<FragmentAwesomeBinding>() {
} }
} }
private fun PendingTransaction.toUiModel() = UiModel().also { model -> private fun PendingTransaction.toUiModel() = UiModel().also { model ->
when { when {
isCancelled() -> { isCancelled() -> {
@ -283,7 +276,7 @@ class AwesomeFragment : BaseFragment<FragmentAwesomeBinding>() {
model.primaryAction = { onCancel(this) } model.primaryAction = { onCancel(this) }
} else { } else {
model.primaryButtonText = "Shielding Funds..." model.primaryButtonText = "Shielding Funds..."
if(isCreated()) model.details.add("Submitting transaction...") if (isCreated()) model.details.add("Submitting transaction...")
} }
} }
} }
@ -301,8 +294,8 @@ class AwesomeFragment : BaseFragment<FragmentAwesomeBinding>() {
val details: MutableSet<String> = linkedSetOf(), val details: MutableSet<String> = linkedSetOf(),
var showProgress: Boolean = false, var showProgress: Boolean = false,
var primaryButtonText: String = "Shield Transparent Funds", var primaryButtonText: String = "Shield Transparent Funds",
var primaryAction: () -> Unit = {}, var primaryAction: () -> Unit = {},
var canCancel: Boolean = false, var canCancel: Boolean = false,
var updateBalance: Boolean = false, var updateBalance: Boolean = false,
) )
} }

View File

@ -3,26 +3,21 @@ package cash.z.ecc.android.ui.profile
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewTreeObserver
import android.view.WindowManager
import android.widget.Toast import android.widget.Toast
import androidx.core.view.doOnLayout
import androidx.navigation.fragment.navArgs import androidx.navigation.fragment.navArgs
import cash.z.ecc.android.R import cash.z.ecc.android.R
import cash.z.ecc.android.databinding.FragmentFeedbackBinding import cash.z.ecc.android.databinding.FragmentFeedbackBinding
import cash.z.ecc.android.di.viewmodel.viewModel
import cash.z.ecc.android.feedback.Report import cash.z.ecc.android.feedback.Report
import cash.z.ecc.android.feedback.Report.Funnel.UserFeedback import cash.z.ecc.android.feedback.Report.Funnel.UserFeedback
import cash.z.ecc.android.feedback.Report.Tap.FEEDBACK_CANCEL import cash.z.ecc.android.feedback.Report.Tap.FEEDBACK_CANCEL
import cash.z.ecc.android.feedback.Report.Tap.FEEDBACK_SUBMIT import cash.z.ecc.android.feedback.Report.Tap.FEEDBACK_SUBMIT
import cash.z.ecc.android.ui.base.BaseFragment import cash.z.ecc.android.ui.base.BaseFragment
/** /**
* Fragment representing the home screen of the app. This is the screen most often seen by the user when launching the * Fragment representing the home screen of the app. This is the screen most often seen by the user when launching the
* application. * application.
*/ */
class FeedbackFragment : BaseFragment<FragmentFeedbackBinding>() { class FeedbackFragment : BaseFragment<FragmentFeedbackBinding>() {
override val screen = Report.Screen.FEEDBACK override val screen = Report.Screen.FEEDBACK
val args: FeedbackFragmentArgs by navArgs() val args: FeedbackFragmentArgs by navArgs()
@ -68,7 +63,6 @@ class FeedbackFragment : BaseFragment<FragmentFeedbackBinding>() {
} }
} }
// //
// Private API // Private API
// //

View File

@ -40,7 +40,6 @@ import cash.z.ecc.android.ui.util.DebugFileTwig
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import java.io.File import java.io.File
class ProfileFragment : BaseFragment<FragmentProfileBinding>() { class ProfileFragment : BaseFragment<FragmentProfileBinding>() {
override val screen = Report.Screen.PROFILE override val screen = Report.Screen.PROFILE
@ -93,13 +92,14 @@ class ProfileFragment : BaseFragment<FragmentProfileBinding>() {
if (viewModel.isEasterEggTriggered()) { if (viewModel.isEasterEggTriggered()) {
binding.iconProfile.setImageResource(R.drawable.ic_profile_zebra_02) binding.iconProfile.setImageResource(R.drawable.ic_profile_zebra_02)
} }
} }
private fun onEnterAwesomeMode() { private fun onEnterAwesomeMode() {
(context as? MainActivity)?.safeNavigate(R.id.action_nav_profile_to_nav_awesome) (context as? MainActivity)?.safeNavigate(R.id.action_nav_profile_to_nav_awesome)
?: throw IllegalStateException("Cannot navigate from this activity. " + ?: throw IllegalStateException(
"Expected MainActivity but found ${context?.javaClass?.simpleName}") "Cannot navigate from this activity. " +
"Expected MainActivity but found ${context?.javaClass?.simpleName}"
)
} }
override fun onResume() { override fun onResume() {
@ -135,7 +135,7 @@ class ProfileFragment : BaseFragment<FragmentProfileBinding>() {
viewModel.quickRescan() viewModel.quickRescan()
Toast.makeText(ZcashWalletApp.instance, "Performing quick rescan!", Toast.LENGTH_LONG).show() Toast.makeText(ZcashWalletApp.instance, "Performing quick rescan!", Toast.LENGTH_LONG).show()
mainActivity?.navController?.popBackStack() mainActivity?.navController?.popBackStack()
} catch(t: Throwable) { } catch (t: Throwable) {
mainActivity?.showCriticalMessage("Quick Rescan Failed", "Unable to perform quick rescan due to error:\n\n${t.message}") mainActivity?.showCriticalMessage("Quick Rescan Failed", "Unable to perform quick rescan due to error:\n\n${t.message}")
} }
} }
@ -145,8 +145,8 @@ class ProfileFragment : BaseFragment<FragmentProfileBinding>() {
mainActivity?.showConfirmation( mainActivity?.showConfirmation(
"Are you sure?", "Are you sure?",
"Wiping your data will close the app. Since your seed is preserved, " + "Wiping your data will close the app. Since your seed is preserved, " +
"this operation is probably safe but please backup your seed anyway." + "this operation is probably safe but please backup your seed anyway." +
"\n\nContinue?", "\n\nContinue?",
"Wipe" "Wipe"
) { ) {
viewModel.wipe() viewModel.wipe()
@ -214,4 +214,4 @@ class ProfileFragment : BaseFragment<FragmentProfileBinding>() {
private fun developerLogFile(): File? { private fun developerLogFile(): File? {
return Bush.trunk.find<DebugFileTwig>()?.file return Bush.trunk.find<DebugFileTwig>()?.file
} }
} }

View File

@ -6,7 +6,6 @@ import cash.z.ecc.android.ZcashWalletApp
import cash.z.ecc.android.ext.Const import cash.z.ecc.android.ext.Const
import cash.z.ecc.android.lockbox.LockBox import cash.z.ecc.android.lockbox.LockBox
import cash.z.ecc.android.sdk.Initializer import cash.z.ecc.android.sdk.Initializer
import cash.z.ecc.android.sdk.SdkSynchronizer
import cash.z.ecc.android.sdk.Synchronizer import cash.z.ecc.android.sdk.Synchronizer
import cash.z.ecc.android.sdk.db.entity.PendingTransaction import cash.z.ecc.android.sdk.db.entity.PendingTransaction
import cash.z.ecc.android.sdk.ext.ZcashSdk import cash.z.ecc.android.sdk.ext.ZcashSdk
@ -15,7 +14,6 @@ import cash.z.ecc.android.sdk.tool.DerivationTool
import cash.z.ecc.android.sdk.type.WalletBalance import cash.z.ecc.android.sdk.type.WalletBalance
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Named import javax.inject.Named
@ -108,7 +106,7 @@ class ProfileViewModel @Inject constructor() : ViewModel() {
fun quickScanDistance(): Int { fun quickScanDistance(): Int {
val latest = synchronizer.latestHeight val latest = synchronizer.latestHeight
val oneWeek = 60*60*24/75 * 7 // a week's worth of blocks val oneWeek = 60 * 60 * 24 / 75 * 7 // a week's worth of blocks
var foo = 0 var foo = 0
runBlocking { runBlocking {
foo = synchronizer.getNearestRewindHeight(latest - oneWeek) foo = synchronizer.getNearestRewindHeight(latest - oneWeek)

View File

@ -8,20 +8,16 @@ import android.view.LayoutInflater
import android.view.View import android.view.View
import android.widget.TextView import android.widget.TextView
import cash.z.android.qrecycler.QRecycler import cash.z.android.qrecycler.QRecycler
import cash.z.ecc.android.R
import cash.z.ecc.android.databinding.FragmentReceiveNewBinding import cash.z.ecc.android.databinding.FragmentReceiveNewBinding
import cash.z.ecc.android.di.viewmodel.viewModel import cash.z.ecc.android.di.viewmodel.viewModel
import cash.z.ecc.android.ext.distribute import cash.z.ecc.android.ext.distribute
import cash.z.ecc.android.ext.onClickNavBack import cash.z.ecc.android.ext.onClickNavBack
import cash.z.ecc.android.ext.onClickNavTo
import cash.z.ecc.android.feedback.Report import cash.z.ecc.android.feedback.Report
import cash.z.ecc.android.feedback.Report.Tap.* import cash.z.ecc.android.feedback.Report.Tap.RECEIVE_BACK
import cash.z.ecc.android.ui.base.BaseFragment
import cash.z.ecc.android.sdk.ext.toAbbreviatedAddress
import cash.z.ecc.android.sdk.ext.twig import cash.z.ecc.android.sdk.ext.twig
import cash.z.ecc.android.ui.base.BaseFragment
import cash.z.ecc.android.ui.util.AddressPartNumberSpan import cash.z.ecc.android.ui.util.AddressPartNumberSpan
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlin.math.roundToInt
class ReceiveFragment : BaseFragment<FragmentReceiveNewBinding>() { class ReceiveFragment : BaseFragment<FragmentReceiveNewBinding>() {
override val screen = Report.Screen.RECEIVE override val screen = Report.Screen.RECEIVE
@ -84,4 +80,4 @@ class ReceiveFragment : BaseFragment<FragmentReceiveNewBinding>() {
addressParts[index].text = textSpan addressParts[index].text = textSpan
} }
} }

View File

@ -10,7 +10,6 @@ import com.google.zxing.Reader
import com.google.zxing.common.HybridBinarizer import com.google.zxing.common.HybridBinarizer
import com.google.zxing.qrcode.QRCodeReader import com.google.zxing.qrcode.QRCodeReader
class QrAnalyzer(val scanCallback: (qrContent: String, image: ImageProxy) -> Unit) : class QrAnalyzer(val scanCallback: (qrContent: String, image: ImageProxy) -> Unit) :
ImageAnalysis.Analyzer { ImageAnalysis.Analyzer {
@ -63,5 +62,4 @@ class QrAnalyzer(val scanCallback: (qrContent: String, image: ImageProxy) -> Uni
private fun onImageScan(result: String, image: ImageProxy) { private fun onImageScan(result: String, image: ImageProxy) {
scanCallback(result, image) scanCallback(result, image)
} }
} }

View File

@ -6,7 +6,11 @@ import android.os.Bundle
import android.util.DisplayMetrics import android.util.DisplayMetrics
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import androidx.camera.core.* import androidx.camera.core.AspectRatio
import androidx.camera.core.CameraSelector
import androidx.camera.core.ImageAnalysis
import androidx.camera.core.ImageProxy
import androidx.camera.core.Preview
import androidx.camera.lifecycle.ProcessCameraProvider import androidx.camera.lifecycle.ProcessCameraProvider
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import cash.z.ecc.android.R import cash.z.ecc.android.R
@ -16,7 +20,6 @@ import cash.z.ecc.android.di.viewmodel.viewModel
import cash.z.ecc.android.ext.onClickNavBack import cash.z.ecc.android.ext.onClickNavBack
import cash.z.ecc.android.feedback.Report import cash.z.ecc.android.feedback.Report
import cash.z.ecc.android.feedback.Report.Tap.SCAN_BACK import cash.z.ecc.android.feedback.Report.Tap.SCAN_BACK
import cash.z.ecc.android.sdk.ext.ZcashSdk
import cash.z.ecc.android.sdk.ext.twig import cash.z.ecc.android.sdk.ext.twig
import cash.z.ecc.android.ui.base.BaseFragment import cash.z.ecc.android.ui.base.BaseFragment
import cash.z.ecc.android.ui.send.SendViewModel import cash.z.ecc.android.ui.send.SendViewModel
@ -26,7 +29,9 @@ import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors import java.util.concurrent.Executors
class ScanFragment : BaseFragment<FragmentScanBinding>() { class ScanFragment : BaseFragment<FragmentScanBinding>() {
override val screen = Report.Screen.SCAN override val screen = Report.Screen.SCAN
private val viewModel: ScanViewModel by viewModel() private val viewModel: ScanViewModel by viewModel()
private val sendViewModel: SendViewModel by activityViewModel() private val sendViewModel: SendViewModel by activityViewModel()
@ -54,9 +59,12 @@ class ScanFragment : BaseFragment<FragmentScanBinding>() {
override fun onAttach(context: Context) { override fun onAttach(context: Context) {
super.onAttach(context) super.onAttach(context)
cameraProviderFuture = ProcessCameraProvider.getInstance(context) cameraProviderFuture = ProcessCameraProvider.getInstance(context)
cameraProviderFuture.addListener(Runnable { cameraProviderFuture.addListener(
bindPreview(cameraProviderFuture.get()) Runnable {
}, ContextCompat.getMainExecutor(context)) bindPreview(cameraProviderFuture.get())
},
ContextCompat.getMainExecutor(context)
)
} }
override fun onDestroyView() { override fun onDestroyView() {
@ -87,9 +95,12 @@ class ScanFragment : BaseFragment<FragmentScanBinding>() {
.setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST) .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
.build() .build()
imageAnalysis.setAnalyzer(cameraExecutor!!, QrAnalyzer { q, i -> imageAnalysis.setAnalyzer(
onQrScanned(q, i) cameraExecutor!!,
}) QrAnalyzer { q, i ->
onQrScanned(q, i)
}
)
// Must unbind the use-cases before rebinding them // Must unbind the use-cases before rebinding them
cameraProvider.unbindAll() cameraProvider.unbindAll()
@ -102,7 +113,6 @@ class ScanFragment : BaseFragment<FragmentScanBinding>() {
mainActivity?.feedback?.report(t) mainActivity?.feedback?.report(t)
twig("Error while opening the camera: $t") twig("Error while opening the camera: $t")
} }
} }
/** /**
@ -114,7 +124,8 @@ class ScanFragment : BaseFragment<FragmentScanBinding>() {
height height
) )
if (kotlin.math.abs(previewRatio - (4.0 / 3.0)) if (kotlin.math.abs(previewRatio - (4.0 / 3.0))
<= kotlin.math.abs(previewRatio - (16.0 / 9.0))) { <= kotlin.math.abs(previewRatio - (16.0 / 9.0))
) {
return AspectRatio.RATIO_4_3 return AspectRatio.RATIO_4_3
} }
return AspectRatio.RATIO_16_9 return AspectRatio.RATIO_16_9
@ -127,7 +138,7 @@ class ScanFragment : BaseFragment<FragmentScanBinding>() {
val network = viewModel.networkName val network = viewModel.networkName
binding.textScanError.text = getString(R.string.scan_invalid_address, network, qrContent) binding.textScanError.text = getString(R.string.scan_invalid_address, network, qrContent)
image.close() image.close()
} else { /* continue scanning*/ } else { /* continue scanning*/
binding.textScanError.text = "" binding.textScanError.text = ""
sendViewModel.toAddress = parsed sendViewModel.toAddress = parsed
mainActivity?.safeNavigate(R.id.action_nav_scan_to_nav_send) mainActivity?.safeNavigate(R.id.action_nav_scan_to_nav_send)
@ -157,12 +168,6 @@ class ScanFragment : BaseFragment<FragmentScanBinding>() {
// overlay.set(list) // overlay.set(list)
// } // }
// //
// Permissions // Permissions
// //
@ -171,7 +176,7 @@ class ScanFragment : BaseFragment<FragmentScanBinding>() {
get() { get() {
return try { return try {
val info = mainActivity?.packageManager val info = mainActivity?.packageManager
?.getPackageInfo(mainActivity?.packageName, PackageManager.GET_PERMISSIONS) ?.getPackageInfo(mainActivity?.packageName ?: "", PackageManager.GET_PERMISSIONS)
val ps = info?.requestedPermissions val ps = info?.requestedPermissions
if (ps != null && ps.isNotEmpty()) { if (ps != null && ps.isNotEmpty()) {
ps ps
@ -212,4 +217,4 @@ class ScanFragment : BaseFragment<FragmentScanBinding>() {
return ContextCompat.checkSelfPermission(context, permission) == PackageManager.PERMISSION_GRANTED return ContextCompat.checkSelfPermission(context, permission) == PackageManager.PERMISSION_GRANTED
} }
} }
} }

View File

@ -13,7 +13,7 @@ class ScanViewModel @Inject constructor() : ViewModel() {
val networkName get() = synchronizer.network.networkName val networkName get() = synchronizer.network.networkName
suspend fun parse(qrCode: String): String? { suspend fun parse(qrCode: String): String? {
//temporary parse code to allow both plain addresses and those that start with zcash: // temporary parse code to allow both plain addresses and those that start with zcash:
// TODO: replace with more robust ZIP-321 handling of QR codes // TODO: replace with more robust ZIP-321 handling of QR codes
val address = if (qrCode.startsWith("zcash:")) { val address = if (qrCode.startsWith("zcash:")) {
qrCode.substring(6, qrCode.indexOf("?").takeUnless { it == -1 } ?: qrCode.length) qrCode.substring(6, qrCode.indexOf("?").takeUnless { it == -1 } ?: qrCode.length)
@ -27,5 +27,4 @@ class ScanViewModel @Inject constructor() : ViewModel() {
super.onCleared() super.onCleared()
twig("${javaClass.simpleName} cleared!") twig("${javaClass.simpleName} cleared!")
} }
} }

View File

@ -4,18 +4,15 @@ import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import cash.z.ecc.android.R
import cash.z.ecc.android.databinding.FragmentSendConfirmBinding import cash.z.ecc.android.databinding.FragmentSendConfirmBinding
import cash.z.ecc.android.di.viewmodel.activityViewModel import cash.z.ecc.android.di.viewmodel.activityViewModel
import cash.z.ecc.android.ext.WalletZecFormmatter import cash.z.ecc.android.ext.WalletZecFormmatter
import cash.z.ecc.android.ext.goneIf import cash.z.ecc.android.ext.goneIf
import cash.z.ecc.android.ext.onClickNavTo
import cash.z.ecc.android.feedback.Report import cash.z.ecc.android.feedback.Report
import cash.z.ecc.android.feedback.Report.Funnel.Send import cash.z.ecc.android.feedback.Report.Funnel.Send
import cash.z.ecc.android.feedback.Report.Tap.* import cash.z.ecc.android.feedback.Report.Tap.SEND_CONFIRM_NEXT
import cash.z.ecc.android.ui.base.BaseFragment
import cash.z.ecc.android.sdk.ext.toAbbreviatedAddress import cash.z.ecc.android.sdk.ext.toAbbreviatedAddress
import cash.z.ecc.android.sdk.ext.convertZatoshiToZecString import cash.z.ecc.android.ui.base.BaseFragment
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
class SendConfirmFragment : BaseFragment<FragmentSendConfirmBinding>() { class SendConfirmFragment : BaseFragment<FragmentSendConfirmBinding>() {
@ -49,4 +46,4 @@ class SendConfirmFragment : BaseFragment<FragmentSendConfirmBinding>() {
sendViewModel.funnel(Send.ConfirmPageComplete) sendViewModel.funnel(Send.ConfirmPageComplete)
// mainActivity?.safeNavigate(R.id.action_nav_send_confirm_to_send_final) // mainActivity?.safeNavigate(R.id.action_nav_send_confirm_to_send_final)
} }
} }

View File

@ -13,7 +13,12 @@ import cash.z.ecc.android.ext.WalletZecFormmatter
import cash.z.ecc.android.ext.goneIf import cash.z.ecc.android.ext.goneIf
import cash.z.ecc.android.feedback.Report import cash.z.ecc.android.feedback.Report
import cash.z.ecc.android.feedback.Report.Tap.SEND_FINAL_CLOSE import cash.z.ecc.android.feedback.Report.Tap.SEND_FINAL_CLOSE
import cash.z.ecc.android.sdk.db.entity.* import cash.z.ecc.android.sdk.db.entity.PendingTransaction
import cash.z.ecc.android.sdk.db.entity.isCancelled
import cash.z.ecc.android.sdk.db.entity.isCreating
import cash.z.ecc.android.sdk.db.entity.isFailedEncoding
import cash.z.ecc.android.sdk.db.entity.isFailure
import cash.z.ecc.android.sdk.db.entity.isSubmitSuccess
import cash.z.ecc.android.sdk.ext.toAbbreviatedAddress import cash.z.ecc.android.sdk.ext.toAbbreviatedAddress
import cash.z.ecc.android.sdk.ext.twig import cash.z.ecc.android.sdk.ext.twig
import cash.z.ecc.android.ui.base.BaseFragment import cash.z.ecc.android.ui.base.BaseFragment
@ -70,13 +75,13 @@ class SendFinalFragment : BaseFragment<FragmentSendFinalBinding>() {
} }
buttonMoreInfo.apply { buttonMoreInfo.apply {
setOnClickListener{ setOnClickListener {
val moreInfoMsg = """${getString(R.string.more_info)} : ${model.errorDescription}""" val moreInfoMsg = """${getString(R.string.more_info)} : ${model.errorDescription}"""
txtMoreInfo.run { txtMoreInfo.run {
text = moreInfoMsg text = moreInfoMsg
} }
if(model.errorDescription.isNotEmpty()) if (model.errorDescription.isNotEmpty())
buttonMoreInfo.text = getString(R.string.translated_button_done) buttonMoreInfo.text = getString(R.string.translated_button_done)
} }
} }
@ -115,7 +120,7 @@ class SendFinalFragment : BaseFragment<FragmentSendFinalBinding>() {
private fun PendingTransaction.toUiModel() = UiModel().also { model -> private fun PendingTransaction.toUiModel() = UiModel().also { model ->
when { when {
isCancelled() -> { isCancelled() -> {
model.title = getString(R.string.send_final_result_cancelled) model.title = getString(R.string.send_final_result_cancelled)
model.primaryButtonText = getString(R.string.send_final_button_primary_back) model.primaryButtonText = getString(R.string.send_final_button_primary_back)
model.primaryAction = { onReturnToSend() } model.primaryAction = { onReturnToSend() }
@ -128,7 +133,8 @@ class SendFinalFragment : BaseFragment<FragmentSendFinalBinding>() {
isFailure() -> { isFailure() -> {
model.title = getString(R.string.send_final_button_primary_failed) model.title = getString(R.string.send_final_button_primary_failed)
model.errorMessage = if (isFailedEncoding()) getString(R.string.send_final_error_encoding) else getString( model.errorMessage = if (isFailedEncoding()) getString(R.string.send_final_error_encoding) else getString(
R.string.send_final_error_submitting) R.string.send_final_error_submitting
)
model.errorDescription = errorMessage.toString() model.errorDescription = errorMessage.toString()
model.primaryButtonText = getString(R.string.send_final_button_primary_retry) model.primaryButtonText = getString(R.string.send_final_button_primary_retry)
model.primaryAction = { onReturnToSend() } model.primaryAction = { onReturnToSend() }
@ -156,7 +162,6 @@ class SendFinalFragment : BaseFragment<FragmentSendFinalBinding>() {
var showProgress: Boolean = false, var showProgress: Boolean = false,
var errorMessage: String = "", var errorMessage: String = "",
var primaryButtonText: String = "See Details", var primaryButtonText: String = "See Details",
var primaryAction: () -> Unit = {} var primaryAction: () -> Unit = {}
) )
} }

View File

@ -19,10 +19,24 @@ import androidx.lifecycle.viewModelScope
import cash.z.ecc.android.R import cash.z.ecc.android.R
import cash.z.ecc.android.databinding.FragmentSendBinding import cash.z.ecc.android.databinding.FragmentSendBinding
import cash.z.ecc.android.di.viewmodel.activityViewModel import cash.z.ecc.android.di.viewmodel.activityViewModel
import cash.z.ecc.android.ext.* import cash.z.ecc.android.ext.WalletZecFormmatter
import cash.z.ecc.android.ext.gone
import cash.z.ecc.android.ext.goneIf
import cash.z.ecc.android.ext.onClickNavUp
import cash.z.ecc.android.ext.toAppColor
import cash.z.ecc.android.ext.visible
import cash.z.ecc.android.feedback.Report import cash.z.ecc.android.feedback.Report
import cash.z.ecc.android.feedback.Report.Tap.* import cash.z.ecc.android.feedback.Report.Tap.SEND_ADDRESS_BACK
import cash.z.ecc.android.sdk.ext.* import cash.z.ecc.android.feedback.Report.Tap.SEND_ADDRESS_PASTE
import cash.z.ecc.android.feedback.Report.Tap.SEND_ADDRESS_REUSE
import cash.z.ecc.android.feedback.Report.Tap.SEND_ADDRESS_SCAN
import cash.z.ecc.android.feedback.Report.Tap.SEND_MEMO_EXCLUDE
import cash.z.ecc.android.feedback.Report.Tap.SEND_MEMO_INCLUDE
import cash.z.ecc.android.feedback.Report.Tap.SEND_SUBMIT
import cash.z.ecc.android.sdk.ext.ZcashSdk
import cash.z.ecc.android.sdk.ext.collectWith
import cash.z.ecc.android.sdk.ext.onFirstWith
import cash.z.ecc.android.sdk.ext.toAbbreviatedAddress
import cash.z.ecc.android.sdk.type.AddressType import cash.z.ecc.android.sdk.type.AddressType
import cash.z.ecc.android.sdk.type.WalletBalance import cash.z.ecc.android.sdk.type.WalletBalance
import cash.z.ecc.android.ui.base.BaseFragment import cash.z.ecc.android.ui.base.BaseFragment
@ -30,7 +44,8 @@ import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
class SendFragment : BaseFragment<FragmentSendBinding>(), class SendFragment :
BaseFragment<FragmentSendBinding>(),
ClipboardManager.OnPrimaryClipChangedListener { ClipboardManager.OnPrimaryClipChangedListener {
override val screen = Report.Screen.SEND_ADDRESS override val screen = Report.Screen.SEND_ADDRESS
@ -49,14 +64,13 @@ class SendFragment : BaseFragment<FragmentSendBinding>(),
applyViewModel(sendViewModel) applyViewModel(sendViewModel)
updateAddressUi(false) updateAddressUi(false)
// Apply behaviors // Apply behaviors
binding.buttonSend.setOnClickListener { binding.buttonSend.setOnClickListener {
onSubmit().also { tapped(SEND_SUBMIT) } onSubmit().also { tapped(SEND_SUBMIT) }
} }
binding.checkIncludeAddress.setOnCheckedChangeListener { _, _-> binding.checkIncludeAddress.setOnCheckedChangeListener { _, _ ->
onIncludeMemo(binding.checkIncludeAddress.isChecked) onIncludeMemo(binding.checkIncludeAddress.isChecked)
} }
@ -172,8 +186,7 @@ class SendFragment : BaseFragment<FragmentSendBinding>(),
} }
/** /**
* To hide input Memo and reply-to option for T type address and show a info message about memo option availability * To hide input Memo and reply-to option for T type address and show a info message about memo option availability */
* */
private fun updateAddressUi(isMemoHidden: Boolean) { private fun updateAddressUi(isMemoHidden: Boolean) {
if (isMemoHidden) { if (isMemoHidden) {
binding.textLayoutMemo.gone() binding.textLayoutMemo.gone()
@ -186,7 +199,6 @@ class SendFragment : BaseFragment<FragmentSendBinding>(),
} }
} }
private fun onSubmit(unused: EditText? = null) { private fun onSubmit(unused: EditText? = null) {
sendViewModel.toAddress = binding.inputZcashAddress.text.toString() sendViewModel.toAddress = binding.inputZcashAddress.text.toString()
sendViewModel.validate(requireContext(), availableZatoshi, maxZatoshi).onFirstWith(resumedScope) { errorMessage -> sendViewModel.validate(requireContext(), availableZatoshi, maxZatoshi).onFirstWith(resumedScope) { errorMessage ->
@ -217,7 +229,6 @@ class SendFragment : BaseFragment<FragmentSendBinding>(),
} }
} }
override fun onAttach(context: Context) { override fun onAttach(context: Context) {
super.onAttach(context) super.onAttach(context)
mainActivity?.clipboard?.addPrimaryClipChangedListener(this) mainActivity?.clipboard?.addPrimaryClipChangedListener(this)
@ -280,7 +291,8 @@ class SendFragment : BaseFragment<FragmentSendBinding>(),
imageLastUsedShield, imageLastUsedShield,
lastUsedAddressLabel, lastUsedAddressLabel,
selected, selected,
address.takeUnless { isBoth }) address.takeUnless { isBoth }
)
} }
binding.dividerClipboard.setText(if (isBoth) R.string.send_history_last_and_clipboard else R.string.send_history_clipboard) binding.dividerClipboard.setText(if (isBoth) R.string.send_history_last_and_clipboard else R.string.send_history_clipboard)
} }
@ -306,8 +318,8 @@ class SendFragment : BaseFragment<FragmentSendBinding>(),
ImageViewCompat.setImageTintList(shieldIcon, ColorStateList.valueOf(if (selected) R.color.colorPrimary.toAppColor() else R.color.zcashWhite_12.toAppColor())) ImageViewCompat.setImageTintList(shieldIcon, ColorStateList.valueOf(if (selected) R.color.colorPrimary.toAppColor() else R.color.zcashWhite_12.toAppColor()))
addressLabel.setText(if (address == userShieldedAddr) R.string.send_banner_address_user else R.string.send_banner_address_unknown) addressLabel.setText(if (address == userShieldedAddr) R.string.send_banner_address_user else R.string.send_banner_address_unknown)
if (address == userTransparentAddr) addressLabel.setText("Your Auto-Shielding Address") if (address == userTransparentAddr) addressLabel.setText("Your Auto-Shielding Address")
addressLabel.setTextColor(if(selected) R.color.colorPrimary.toAppColor() else R.color.text_light.toAppColor()) addressLabel.setTextColor(if (selected) R.color.colorPrimary.toAppColor() else R.color.text_light.toAppColor())
addressTextView.setTextColor(if(selected) R.color.text_light.toAppColor() else R.color.text_light_dimmed.toAppColor()) addressTextView.setTextColor(if (selected) R.color.text_light.toAppColor() else R.color.text_light_dimmed.toAppColor())
} }
} }
} }
@ -352,7 +364,6 @@ class SendFragment : BaseFragment<FragmentSendBinding>(),
return lastUsedAddress return lastUsedAddress
} }
private fun ClipboardManager.text(): CharSequence = private fun ClipboardManager.text(): CharSequence =
primaryClip!!.getItemAt(0).coerceToText(mainActivity) primaryClip!!.getItemAt(0).coerceToText(mainActivity)
} }

View File

@ -12,7 +12,11 @@ import cash.z.ecc.android.ext.goneIf
import cash.z.ecc.android.ext.onEditorActionDone import cash.z.ecc.android.ext.onEditorActionDone
import cash.z.ecc.android.feedback.Report import cash.z.ecc.android.feedback.Report
import cash.z.ecc.android.feedback.Report.Funnel.Send import cash.z.ecc.android.feedback.Report.Funnel.Send
import cash.z.ecc.android.feedback.Report.Tap.* import cash.z.ecc.android.feedback.Report.Tap.SEND_MEMO_CLEAR
import cash.z.ecc.android.feedback.Report.Tap.SEND_MEMO_EXCLUDE
import cash.z.ecc.android.feedback.Report.Tap.SEND_MEMO_INCLUDE
import cash.z.ecc.android.feedback.Report.Tap.SEND_MEMO_NEXT
import cash.z.ecc.android.feedback.Report.Tap.SEND_MEMO_SKIP
import cash.z.ecc.android.ui.base.BaseFragment import cash.z.ecc.android.ui.base.BaseFragment
import cash.z.ecc.android.ui.util.INCLUDE_MEMO_PREFIX_STANDARD import cash.z.ecc.android.ui.util.INCLUDE_MEMO_PREFIX_STANDARD
@ -41,7 +45,7 @@ class SendMemoFragment : BaseFragment<FragmentSendMemoBinding>() {
// onBackPressNavTo(it) { tapped(SEND_MEMO_BACK) } // onBackPressNavTo(it) { tapped(SEND_MEMO_BACK) }
// } // }
binding.checkIncludeAddress.setOnCheckedChangeListener { _, _-> binding.checkIncludeAddress.setOnCheckedChangeListener { _, _ ->
onIncludeMemo(binding.checkIncludeAddress.isChecked) onIncludeMemo(binding.checkIncludeAddress.isChecked)
} }
@ -49,7 +53,7 @@ class SendMemoFragment : BaseFragment<FragmentSendMemoBinding>() {
memo.onEditorActionDone { memo.onEditorActionDone {
onTopButton().also { tapped(SEND_MEMO_NEXT) } onTopButton().also { tapped(SEND_MEMO_NEXT) }
} }
memo.doAfterTextChanged { memo.doAfterTextChanged {
binding.clearMemo.goneIf(memo.text.isEmpty()) binding.clearMemo.goneIf(memo.text.isEmpty())
} }
} }
@ -117,4 +121,4 @@ class SendMemoFragment : BaseFragment<FragmentSendMemoBinding>() {
sendViewModel.funnel(Send.MemoPageComplete) sendViewModel.funnel(Send.MemoPageComplete)
// mainActivity?.safeNavigate(R.id.action_nav_send_memo_to_send_confirm) // mainActivity?.safeNavigate(R.id.action_nav_send_memo_to_send_confirm)
} }
} }

View File

@ -15,10 +15,20 @@ import cash.z.ecc.android.feedback.Report.Funnel.Send.SendSelected
import cash.z.ecc.android.feedback.Report.Funnel.Send.SpendingKeyFound import cash.z.ecc.android.feedback.Report.Funnel.Send.SpendingKeyFound
import cash.z.ecc.android.feedback.Report.Issue import cash.z.ecc.android.feedback.Report.Issue
import cash.z.ecc.android.feedback.Report.MetricType import cash.z.ecc.android.feedback.Report.MetricType
import cash.z.ecc.android.feedback.Report.MetricType.* import cash.z.ecc.android.feedback.Report.MetricType.TRANSACTION_CREATED
import cash.z.ecc.android.feedback.Report.MetricType.TRANSACTION_INITIALIZED
import cash.z.ecc.android.feedback.Report.MetricType.TRANSACTION_MINED
import cash.z.ecc.android.feedback.Report.MetricType.TRANSACTION_SUBMITTED
import cash.z.ecc.android.lockbox.LockBox import cash.z.ecc.android.lockbox.LockBox
import cash.z.ecc.android.sdk.Synchronizer import cash.z.ecc.android.sdk.Synchronizer
import cash.z.ecc.android.sdk.db.entity.* import cash.z.ecc.android.sdk.db.entity.PendingTransaction
import cash.z.ecc.android.sdk.db.entity.isCancelled
import cash.z.ecc.android.sdk.db.entity.isCreated
import cash.z.ecc.android.sdk.db.entity.isCreating
import cash.z.ecc.android.sdk.db.entity.isFailedEncoding
import cash.z.ecc.android.sdk.db.entity.isFailedSubmit
import cash.z.ecc.android.sdk.db.entity.isMined
import cash.z.ecc.android.sdk.db.entity.isSubmitSuccess
import cash.z.ecc.android.sdk.ext.ZcashSdk import cash.z.ecc.android.sdk.ext.ZcashSdk
import cash.z.ecc.android.sdk.ext.twig import cash.z.ecc.android.sdk.ext.twig
import cash.z.ecc.android.sdk.tool.DerivationTool import cash.z.ecc.android.sdk.tool.DerivationTool
@ -54,12 +64,12 @@ class SendViewModel @Inject constructor() : ViewModel() {
set(value) { set(value) {
require(!value || (value && !fromAddress.isNullOrEmpty())) { require(!value || (value && !fromAddress.isNullOrEmpty())) {
"Error: fromAddress was empty while attempting to include it in the memo. Verify" + "Error: fromAddress was empty while attempting to include it in the memo. Verify" +
" that initFromAddress() has previously been called on this viewmodel." " that initFromAddress() has previously been called on this viewmodel."
} }
field = value field = value
} }
val isShielded get() = toAddress.startsWith("z") val isShielded get() = toAddress.startsWith("z")
fun send(): Flow<PendingTransaction> { fun send(): Flow<PendingTransaction> {
funnel(SendSelected) funnel(SendSelected)
val memoToSend = createMemoToSend() val memoToSend = createMemoToSend()
@ -92,7 +102,7 @@ class SendViewModel @Inject constructor() : ViewModel() {
suspend fun validateAddress(address: String): AddressType = suspend fun validateAddress(address: String): AddressType =
synchronizer.validateAddress(address) synchronizer.validateAddress(address)
suspend fun isValidAddress(address: String): Boolean = when(validateAddress(address)) { suspend fun isValidAddress(address: String): Boolean = when (validateAddress(address)) {
is AddressType.Shielded, is AddressType.Transparent -> true is AddressType.Shielded, is AddressType.Transparent -> true
else -> false else -> false
} }
@ -140,7 +150,6 @@ class SendViewModel @Inject constructor() : ViewModel() {
includeFromAddress = false includeFromAddress = false
} }
// //
// Analytics // Analytics
// //
@ -176,7 +185,7 @@ class SendViewModel @Inject constructor() : ViewModel() {
} }
} }
private fun updateMetrics(tx: PendingTransaction) { fun updateMetrics(tx: PendingTransaction) {
try { try {
when { when {
tx.isMined() -> TRANSACTION_SUBMITTED to TRANSACTION_MINED by tx.id tx.isMined() -> TRANSACTION_SUBMITTED to TRANSACTION_MINED by tx.id
@ -192,7 +201,7 @@ class SendViewModel @Inject constructor() : ViewModel() {
} }
} }
private fun report(metricId: String?) { fun report(metricId: String?) {
metrics[metricId]?.let { metric -> metrics[metricId]?.let { metric ->
metric.takeUnless { (it.elapsedTime ?: 0) <= 0L }?.let { metric.takeUnless { (it.elapsedTime ?: 0) <= 0L }?.let {
viewModelScope.launch { viewModelScope.launch {
@ -228,26 +237,17 @@ class SendViewModel @Inject constructor() : ViewModel() {
metrics[metricId].also { if (it == null) println("Warning no start metric for id: $metricId") } metrics[metricId].also { if (it == null) println("Warning no start metric for id: $metricId") }
} }
return startMetric?.endTime?.let { startMetricEndTime -> return startMetric?.endTime?.let { startMetricEndTime ->
TimeMetric(second.key, second.description, mutableListOf(startMetricEndTime)) TimeMetric(second.key, second.description, mutableListOf(startMetricEndTime))
.markTime().let { endMetric -> .markTime().let { endMetric ->
endMetric.toMetricIdFor(txId).also { metricId -> endMetric.toMetricIdFor(txId).also { metricId ->
metrics[metricId] = endMetric metrics[metricId] = endMetric
metrics[metricId.toRelatedMetricId()] = startMetric metrics[metricId.toRelatedMetricId()] = startMetric
}
} }
} }
}
} }
private fun Keyed<String>.toMetricIdFor(id: Long): String = "$id.$key" private fun Keyed<String>.toMetricIdFor(id: Long): String = "$id.$key"
private fun String.toRelatedMetricId(): String = "$this.related" private fun String.toRelatedMetricId(): String = "$this.related"
private fun String.toTxId(): Long = split('.').first().toLong() private fun String.toTxId(): Long = split('.').first().toLong()
} }

View File

@ -10,7 +10,13 @@ import cash.z.ecc.android.R
import cash.z.ecc.android.ZcashWalletApp import cash.z.ecc.android.ZcashWalletApp
import cash.z.ecc.android.databinding.FragmentSettingsBinding import cash.z.ecc.android.databinding.FragmentSettingsBinding
import cash.z.ecc.android.di.viewmodel.viewModel import cash.z.ecc.android.di.viewmodel.viewModel
import cash.z.ecc.android.ext.* import cash.z.ecc.android.ext.gone
import cash.z.ecc.android.ext.onClickNavBack
import cash.z.ecc.android.ext.showUpdateServerCriticalError
import cash.z.ecc.android.ext.showUpdateServerDialog
import cash.z.ecc.android.ext.toAppColor
import cash.z.ecc.android.ext.toAppString
import cash.z.ecc.android.ext.visible
import cash.z.ecc.android.sdk.exception.LightWalletException import cash.z.ecc.android.sdk.exception.LightWalletException
import cash.z.ecc.android.sdk.ext.collectWith import cash.z.ecc.android.sdk.ext.collectWith
import cash.z.ecc.android.sdk.ext.twig import cash.z.ecc.android.sdk.ext.twig
@ -53,7 +59,6 @@ class SettingsFragment : BaseFragment<FragmentSettingsBinding>() {
viewModel.uiModels.collectWith(resumedScope, ::onUiModelUpdated) viewModel.uiModels.collectWith(resumedScope, ::onUiModelUpdated)
} }
// //
// Event handlers // Event handlers
// //
@ -131,24 +136,22 @@ class SettingsFragment : BaseFragment<FragmentSettingsBinding>() {
error.javaClass.simpleName error.javaClass.simpleName
} }
val message = "An error occured while changing servers. Please verify the info" + val message = "An error occured while changing servers. Please verify the info" +
" and try again.\n\nError: $details" " and try again.\n\nError: $details"
twig(message) twig(message)
Toast.makeText(ZcashWalletApp.instance, getString(R.string.settings_toast_change_server_failure), Toast.LENGTH_SHORT).show() Toast.makeText(ZcashWalletApp.instance, getString(R.string.settings_toast_change_server_failure), Toast.LENGTH_SHORT).show()
context?.showUpdateServerCriticalError(message) context?.showUpdateServerCriticalError(message)
} }
// //
// Utilities // Utilities
// //
private fun String?.toHelperTextColor(): ColorStateList { private fun String?.toHelperTextColor(): ColorStateList {
val color = if (this == null) { val color = if (this == null) {
R.color.text_light_dimmed R.color.text_light_dimmed
} else { } else {
R.color.zcashRed R.color.zcashRed
} }
return ColorStateList.valueOf(color.toAppColor()) return ColorStateList.valueOf(color.toAppColor())
} }
} }

View File

@ -6,7 +6,6 @@ import cash.z.ecc.android.lockbox.LockBox
import cash.z.ecc.android.sdk.Synchronizer import cash.z.ecc.android.sdk.Synchronizer
import cash.z.ecc.android.sdk.ext.twig import cash.z.ecc.android.sdk.ext.twig
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.cancellable
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Named import javax.inject.Named
import kotlin.properties.Delegates.observable import kotlin.properties.Delegates.observable
@ -28,7 +27,6 @@ class SettingsViewModel @Inject constructor() : ViewModel() {
var pendingHost by observable("", ::onUpdateModel) var pendingHost by observable("", ::onUpdateModel)
var pendingPortText by observable("", ::onUpdateModel) var pendingPortText by observable("", ::onUpdateModel)
private fun getHost(): String { private fun getHost(): String {
return prefs[Const.Pref.SERVER_HOST] ?: Const.Default.Server.HOST return prefs[Const.Pref.SERVER_HOST] ?: Const.Default.Server.HOST
} }

View File

@ -40,7 +40,7 @@ class BackupFragment : BaseFragment<FragmentBackupBinding>() {
private val walletSetup: WalletSetupViewModel by activityViewModel(false) private val walletSetup: WalletSetupViewModel by activityViewModel(false)
private var hasBackUp: Boolean = true //TODO: implement backup and then check for it here-ish private var hasBackUp: Boolean = true // TODO: implement backup and then check for it here-ish
override fun inflate(inflater: LayoutInflater): FragmentBackupBinding = override fun inflate(inflater: LayoutInflater): FragmentBackupBinding =
FragmentBackupBinding.inflate(inflater) FragmentBackupBinding.inflate(inflater)
@ -76,7 +76,7 @@ class BackupFragment : BaseFragment<FragmentBackupBinding>() {
override fun onAttach(context: Context) { override fun onAttach(context: Context) {
super.onAttach(context) super.onAttach(context)
walletSetup.checkSeed().onEach { walletSetup.checkSeed().onEach {
hasBackUp = when(it) { hasBackUp = when (it) {
SEED_WITH_BACKUP -> true SEED_WITH_BACKUP -> true
else -> false else -> false
} }
@ -134,9 +134,9 @@ class BackupFragment : BaseFragment<FragmentBackupBinding>() {
mainActivity!!.feedback.measure(SEED_PHRASE_LOADED) { mainActivity!!.feedback.measure(SEED_PHRASE_LOADED) {
val lockBox = LockBox(ZcashWalletApp.instance) val lockBox = LockBox(ZcashWalletApp.instance)
val mnemonics = Mnemonics() val mnemonics = Mnemonics()
val seedPhrase = lockBox.getCharsUtf8(Const.Backup.SEED_PHRASE) ?: throw RuntimeException("Seed Phrase expected but not found in storage!!") val seedPhrase = lockBox.getCharsUtf8(Const.Backup.SEED_PHRASE) ?: throw RuntimeException("Seed Phrase expected but not found in storage!!")
val result = mnemonics.toWordList(seedPhrase) val result = mnemonics.toWordList(seedPhrase)
result result
} }
} }
} }

View File

@ -89,7 +89,7 @@ class LandingFragment : BaseFragment<FragmentLandingBinding>() {
super.onAttach(context) super.onAttach(context)
walletSetup.checkSeed().onEach { walletSetup.checkSeed().onEach {
when(it) { when (it) {
SEED_WITHOUT_BACKUP, SEED_WITH_BACKUP -> { SEED_WITHOUT_BACKUP, SEED_WITH_BACKUP -> {
mainActivity?.safeNavigate(R.id.nav_backup) mainActivity?.safeNavigate(R.id.nav_backup)
} }
@ -100,9 +100,12 @@ class LandingFragment : BaseFragment<FragmentLandingBinding>() {
override fun onResume() { override fun onResume() {
super.onResume() super.onResume()
view?.postDelayed({ view?.postDelayed(
mainActivity?.hideKeyboard() {
}, 25L) mainActivity?.hideKeyboard()
},
25L
)
} }
private fun onSkip(count: Int) { private fun onSkip(count: Int) {
@ -134,10 +137,10 @@ class LandingFragment : BaseFragment<FragmentLandingBinding>() {
val birthday: Int val birthday: Int
// new testnet dev wallet // new testnet dev wallet
when(ZcashWalletApp.instance.defaultNetwork) { when (ZcashWalletApp.instance.defaultNetwork) {
ZcashNetwork.Mainnet -> { ZcashNetwork.Mainnet -> {
seedPhrase = "still champion voice habit trend flight survey between bitter process artefact blind carbon truly provide dizzy crush flush breeze blouse charge solid fish spread" seedPhrase = "still champion voice habit trend flight survey between bitter process artefact blind carbon truly provide dizzy crush flush breeze blouse charge solid fish spread"
birthday = 991645 //663174 birthday = 991645 // 663174
} }
ZcashNetwork.Testnet -> { ZcashNetwork.Testnet -> {
seedPhrase = "quantum whisper lion route fury lunar pelican image job client hundred sauce chimney barely life cliff spirit admit weekend message recipe trumpet impact kitten" seedPhrase = "quantum whisper lion route fury lunar pelican image job client hundred sauce chimney barely life cliff spirit admit weekend message recipe trumpet impact kitten"
@ -211,4 +214,4 @@ class LandingFragment : BaseFragment<FragmentLandingBinding>() {
skipCount = 0 skipCount = 0
mainActivity?.navController?.popBackStack() mainActivity?.navController?.popBackStack()
} }
} }

View File

@ -10,7 +10,6 @@ import android.view.MotionEvent
import android.view.MotionEvent.ACTION_DOWN import android.view.MotionEvent.ACTION_DOWN
import android.view.MotionEvent.ACTION_UP import android.view.MotionEvent.ACTION_UP
import android.view.View import android.view.View
import android.widget.RelativeLayout
import android.widget.TextView import android.widget.TextView
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
@ -28,7 +27,6 @@ import cash.z.ecc.android.feedback.Report.Tap.RESTORE_BACK
import cash.z.ecc.android.feedback.Report.Tap.RESTORE_CLEAR import cash.z.ecc.android.feedback.Report.Tap.RESTORE_CLEAR
import cash.z.ecc.android.feedback.Report.Tap.RESTORE_DONE import cash.z.ecc.android.feedback.Report.Tap.RESTORE_DONE
import cash.z.ecc.android.feedback.Report.Tap.RESTORE_SUCCESS import cash.z.ecc.android.feedback.Report.Tap.RESTORE_SUCCESS
import cash.z.ecc.android.sdk.ext.twig
import cash.z.ecc.android.ui.base.BaseFragment import cash.z.ecc.android.ui.base.BaseFragment
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.tylersuehr.chips.Chip import com.tylersuehr.chips.Chip
@ -36,7 +34,6 @@ import com.tylersuehr.chips.ChipsAdapter
import com.tylersuehr.chips.SeedWordAdapter import com.tylersuehr.chips.SeedWordAdapter
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
class RestoreFragment : BaseFragment<FragmentRestoreBinding>(), View.OnKeyListener { class RestoreFragment : BaseFragment<FragmentRestoreBinding>(), View.OnKeyListener {
override val screen = Report.Screen.RESTORE override val screen = Report.Screen.RESTORE
@ -57,7 +54,6 @@ class RestoreFragment : BaseFragment<FragmentRestoreBinding>(), View.OnKeyListen
}.also { onChipsModified() } }.also { onChipsModified() }
seedWordRecycler.adapter = seedWordAdapter seedWordRecycler.adapter = seedWordAdapter
binding.chipsInput.apply { binding.chipsInput.apply {
setFilterableChipList(getChips()) setFilterableChipList(getChips())
setDelimiter("[ ;,]", true) setDelimiter("[ ;,]", true)
@ -116,7 +112,6 @@ class RestoreFragment : BaseFragment<FragmentRestoreBinding>(), View.OnKeyListen
touchScreenForUser() touchScreenForUser()
} }
private fun onExit() { private fun onExit() {
mainActivity?.reportFunnel(Restore.Exit) mainActivity?.reportFunnel(Restore.Exit)
hideAutoCompleteWords() hideAutoCompleteWords()
@ -189,12 +184,15 @@ class RestoreFragment : BaseFragment<FragmentRestoreBinding>(), View.OnKeyListen
// forcefully show the keyboard as a hack to fix odd behavior where the keyboard // forcefully show the keyboard as a hack to fix odd behavior where the keyboard
// sometimes closes randomly and inexplicably in between seed word entries // sometimes closes randomly and inexplicably in between seed word entries
private fun forceShowKeyboard() { private fun forceShowKeyboard() {
requireView().postDelayed({ requireView().postDelayed(
val isDone = (seedWordAdapter?.itemCount ?: 0) > 24 {
val focusedView = if (isDone) binding.inputBirthdate else seedWordAdapter!!.editText val isDone = (seedWordAdapter?.itemCount ?: 0) > 24
mainActivity!!.showKeyboard(focusedView) val focusedView = if (isDone) binding.inputBirthdate else seedWordAdapter!!.editText
focusedView.requestFocus() mainActivity!!.showKeyboard(focusedView)
}, 500L) focusedView.requestFocus()
},
500L
)
} }
private fun reportWords(count: Int) { private fun reportWords(count: Int) {
@ -220,11 +218,14 @@ class RestoreFragment : BaseFragment<FragmentRestoreBinding>(), View.OnKeyListen
private fun touchScreenForUser() { private fun touchScreenForUser() {
seedWordAdapter?.editText?.apply { seedWordAdapter?.editText?.apply {
postDelayed({ postDelayed(
seedWordAdapter?.editText?.inputType = InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS or InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD {
dispatchTouchEvent(motionEvent(ACTION_DOWN)) seedWordAdapter?.editText?.inputType = InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS or InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD
dispatchTouchEvent(motionEvent(ACTION_UP)) dispatchTouchEvent(motionEvent(ACTION_DOWN))
}, 100L) dispatchTouchEvent(motionEvent(ACTION_UP))
},
100L
)
} }
} }
@ -235,13 +236,12 @@ class RestoreFragment : BaseFragment<FragmentRestoreBinding>(), View.OnKeyListen
override fun onKey(v: View?, keyCode: Int, event: KeyEvent?): Boolean { override fun onKey(v: View?, keyCode: Int, event: KeyEvent?): Boolean {
return false return false
} }
} }
class SeedWordChip(val word: String, var index: Int = -1) : Chip() { class SeedWordChip(val word: String, var index: Int = -1) : Chip() {
override fun getSubtitle(): String? = null//"subtitle for $word" override fun getSubtitle(): String? = null // "subtitle for $word"
override fun getAvatarDrawable(): Drawable? = null override fun getAvatarDrawable(): Drawable? = null
override fun getId() = index override fun getId() = index
override fun getTitle() = word override fun getTitle() = word
override fun getAvatarUri() = null override fun getAvatarUri() = null
} }

View File

@ -7,10 +7,9 @@ import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import cash.z.ecc.android.R import cash.z.ecc.android.R
import cash.z.ecc.android.ext.toAppColor import cash.z.ecc.android.ext.toAppColor
import cash.z.ecc.android.sdk.ext.twig
import cash.z.ecc.android.ui.setup.SeedWordChip import cash.z.ecc.android.ui.setup.SeedWordChip
class SeedWordAdapter : ChipsAdapter { class SeedWordAdapter : ChipsAdapter {
constructor(existingAdapter: ChipsAdapter) : super(existingAdapter.mDataSource, existingAdapter.mEditText, existingAdapter.mOptions) constructor(existingAdapter: ChipsAdapter) : super(existingAdapter.mDataSource, existingAdapter.mEditText, existingAdapter.mOptions)
@ -22,23 +21,23 @@ class SeedWordAdapter : ChipsAdapter {
else object : RecyclerView.ViewHolder(mEditText) {} else object : RecyclerView.ViewHolder(mEditText) {}
} }
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
if (getItemViewType(position) == CHIP) { // Chips if (getItemViewType(position) == CHIP) { // Chips
// Display the chip information on the chip view // Display the chip information on the chip view
(holder as SeedWordHolder).seedChipView.bind(mDataSource.getSelectedChip(position), position); (holder as SeedWordHolder).seedChipView.bind(mDataSource.getSelectedChip(position), position)
} else { } else {
val size = mDataSource.selectedChips.size val size = mDataSource.selectedChips.size
// tricky bugfix: // tricky bugfix:
// keep this always enabled otherwise older versions of android crash when this // keep this always enabled otherwise older versions of android crash when this
// view is given focus. As a work around, just hide the cursor when the user is done // view is given focus. As a work around, just hide the cursor when the user is done
// editing. This is not ideal but it's better than a crash during wallet restore! // editing. This is not ideal but it's better than a crash during wallet restore!
mEditText.isEnabled = true mEditText.isEnabled = true
mEditText.hint = if (size < 3) { mEditText.hint = if (size < 3) {
mEditText.isCursorVisible = true mEditText.isCursorVisible = true
mEditText.setHintTextColor(R.color.text_light_dimmed.toAppColor()) mEditText.setHintTextColor(R.color.text_light_dimmed.toAppColor())
val ordinal = when(size) {2 -> "3rd"; 1 -> "2nd"; else -> "1st"} val ordinal = when (size) { 2 -> "3rd"; 1 -> "2nd"; else -> "1st" }
"Enter $ordinal seed word" "Enter $ordinal seed word"
} else if(size >= 24) { } else if (size >= 24) {
mEditText.setHintTextColor(R.color.zcashGreen.toAppColor()) mEditText.setHintTextColor(R.color.zcashGreen.toAppColor())
mEditText.isCursorVisible = false mEditText.isCursorVisible = false
"done" "done"
@ -66,10 +65,13 @@ class SeedWordAdapter : ChipsAdapter {
if (mDataSource.originalChips.firstOrNull { it.title == text } != null) { if (mDataSource.originalChips.firstOrNull { it.title == text } != null) {
mDataSource.addSelectedChip(DefaultCustomChip(text)) mDataSource.addSelectedChip(DefaultCustomChip(text))
mEditText.apply { mEditText.apply {
postDelayed({ postDelayed(
setText("") {
requestFocus() setText("")
}, 50L) requestFocus()
},
50L
)
} }
} }
} }
@ -99,4 +101,3 @@ class SeedWordAdapter : ChipsAdapter {
} }
} }
} }

View File

@ -175,7 +175,6 @@ class WalletSetupViewModel @Inject constructor() : ViewModel() {
private fun loadNearestBirthday(network: ZcashNetwork, birthdayHeight: Int? = null) = private fun loadNearestBirthday(network: ZcashNetwork, birthdayHeight: Int? = null) =
WalletBirthdayTool.loadNearest(ZcashWalletApp.instance, network, birthdayHeight) WalletBirthdayTool.loadNearest(ZcashWalletApp.instance, network, birthdayHeight)
// //
// Storage Helpers // Storage Helpers
// //
@ -193,7 +192,7 @@ class WalletSetupViewModel @Inject constructor() : ViewModel() {
) { ) {
check(!lockBox.getBoolean(Const.Backup.HAS_SEED)) { check(!lockBox.getBoolean(Const.Backup.HAS_SEED)) {
"Error! Cannot store a seed when one already exists! This would overwrite the" + "Error! Cannot store a seed when one already exists! This would overwrite the" +
" existing seed and could lead to a loss of funds if the user has no backup!" " existing seed and could lead to a loss of funds if the user has no backup!"
} }
storeBirthday(birthday) storeBirthday(birthday)
@ -229,5 +228,4 @@ class WalletSetupViewModel @Inject constructor() : ViewModel() {
lockBox[Const.Backup.VIEWING_KEY] = vk.extfvk lockBox[Const.Backup.VIEWING_KEY] = vk.extfvk
lockBox[Const.Backup.PUBLIC_KEY] = vk.extpub lockBox[Const.Backup.PUBLIC_KEY] = vk.extpub
} }
}
}

View File

@ -16,13 +16,13 @@ class AddressPartNumberSpan(
) : MetricAffectingSpan() { ) : MetricAffectingSpan() {
override fun updateMeasureState(textPaint: TextPaint) { override fun updateMeasureState(textPaint: TextPaint) {
textPaint.baselineShift += (textPaint.ascent() / 2).toInt() // from SuperscriptSpan textPaint.baselineShift += (textPaint.ascent() / 2).toInt() // from SuperscriptSpan
textPaint.textSize = textPaint.textSize * proportion // from RelativeSizeSpan textPaint.textSize = textPaint.textSize * proportion // from RelativeSizeSpan
} }
override fun updateDrawState(textPaint: TextPaint) { override fun updateDrawState(textPaint: TextPaint) {
textPaint.baselineShift += (textPaint.ascent() / 2).toInt() // from SuperscriptSpan (baseline must shift before resizing or else it will not properly align to the top of the text) textPaint.baselineShift += (textPaint.ascent() / 2).toInt() // from SuperscriptSpan (baseline must shift before resizing or else it will not properly align to the top of the text)
textPaint.textSize = textPaint.textSize * proportion // from RelativeSizeSpan textPaint.textSize = textPaint.textSize * proportion // from RelativeSizeSpan
textPaint.color = color // from ForegroundColorSpan textPaint.color = color // from ForegroundColorSpan
} }
} }

View File

@ -19,4 +19,4 @@ class DebugFileTwig(fileName: String = "developer_log.txt") : TroubleshootingTwi
it.writeUtf8("$message\n") it.writeUtf8("$message\n")
} }
} }
} }

View File

@ -13,12 +13,12 @@ const val INCLUDE_MEMO_PREFIX_STANDARD = "Reply-To:"
* The non-standard prefixes that we will parse if other wallets send them our way. * The non-standard prefixes that we will parse if other wallets send them our way.
*/ */
val INCLUDE_MEMO_PREFIXES_RECOGNIZED = arrayOf( val INCLUDE_MEMO_PREFIXES_RECOGNIZED = arrayOf(
INCLUDE_MEMO_PREFIX_STANDARD, // standard INCLUDE_MEMO_PREFIX_STANDARD, // standard
"reply-to", // standard w/o colon "reply-to", // standard w/o colon
"reply to:", // space instead of dash "reply to:", // space instead of dash
"reply to", // space instead of dash w/o colon "reply to", // space instead of dash w/o colon
"sent from:", // previous standard "sent from:", // previous standard
"sent from" // previous standard w/o colon "sent from" // previous standard w/o colon
) )
// TODO: move this to the SDK // TODO: move this to the SDK
@ -41,7 +41,7 @@ object MemoUtil {
INCLUDE_MEMO_PREFIXES_RECOGNIZED.mapNotNull { INCLUDE_MEMO_PREFIXES_RECOGNIZED.mapNotNull {
val maybeMemo = memo.substringAfterLast(it) val maybeMemo = memo.substringAfterLast(it)
if (addressValidator(maybeMemo)) maybeMemo else null if (addressValidator(maybeMemo)) maybeMemo else null
}.firstOrNull{ !it.isNullOrBlank() } }.firstOrNull { !it.isNullOrBlank() }
} }
} }

View File

@ -1,15 +1,15 @@
package cash.z.ecc.android.ui.util package cash.z.ecc.android.ui.util
// //
//import android.Manifest // import android.Manifest
//import android.content.Context // import android.content.Context
//import android.content.pm.PackageManager // import android.content.pm.PackageManager
//import android.os.Bundle // import android.os.Bundle
//import android.widget.Toast // import android.widget.Toast
//import androidx.core.content.ContextCompat // import androidx.core.content.ContextCompat
//import androidx.fragment.app.Fragment // import androidx.fragment.app.Fragment
//import cash.z.ecc.android.ui.MainActivity // import cash.z.ecc.android.ui.MainActivity
// //
//class PermissionFragment : Fragment() { // class PermissionFragment : Fragment() {
// //
// val activity get() = context as MainActivity // val activity get() = context as MainActivity
// //
@ -44,4 +44,4 @@ package cash.z.ecc.android.ui.util
// ContextCompat.checkSelfPermission(context, it) == PackageManager.PERMISSION_GRANTED // ContextCompat.checkSelfPermission(context, it) == PackageManager.PERMISSION_GRANTED
// } // }
// } // }
//} // }

View File

@ -1,16 +1,17 @@
package cash.z.ecc.android package cash.z.ecc.android
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.* import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.flow.scanReduce
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertTrue
import org.junit.Test import org.junit.Test
import kotlin.math.round
import kotlin.math.roundToInt
class ScratchPad { class ScratchPad {
val t get() = System.currentTimeMillis() val t get() = System.currentTimeMillis()
var t0 = 0L var t0 = 0L
val Δt get() = t - t0 val Δt get() = t - t0
@ -23,10 +24,10 @@ class ScratchPad {
t0 = t t0 = t
started = true started = true
} }
println("$Δt\temitting $it"); println("$Δt\temitting $it")
} }
val flow2 = flowOf("a", "b", "c", "d", "e", "f").onEach { delay(150); println("$Δt\temitting $it")} val flow2 = flowOf("a", "b", "c", "d", "e", "f").onEach { delay(150); println("$Δt\temitting $it") }
val flow3 = flowOf("A", "B").onEach { delay(450); println("$Δt\temitting $it")} val flow3 = flowOf("A", "B").onEach { delay(450); println("$Δt\temitting $it") }
combine(flow, flow2, flow3) { i, s, t -> "$i$s$t" }.onStart { combine(flow, flow2, flow3) { i, s, t -> "$i$s$t" }.onStart {
t0 = t t0 = t
}.collect { }.collect {
@ -50,5 +51,4 @@ class ScratchPad {
println("got $it") println("got $it")
} }
} }
}
}

View File

@ -2,9 +2,13 @@ package cash.z.ecc.android
import cash.z.ecc.android.feedback.Feedback import cash.z.ecc.android.feedback.Feedback
import cash.z.ecc.android.sdk.db.entity.PendingTransaction import cash.z.ecc.android.sdk.db.entity.PendingTransaction
import cash.z.ecc.android.sdk.db.entity.* import cash.z.ecc.android.sdk.db.entity.isCreated
import cash.z.ecc.android.sdk.db.entity.isCreating
import cash.z.ecc.android.sdk.db.entity.isMined
import cash.z.ecc.android.sdk.db.entity.isSubmitSuccess
import cash.z.ecc.android.ui.send.SendViewModel import cash.z.ecc.android.ui.send.SendViewModel
import com.nhaarman.mockitokotlin2.* import com.nhaarman.mockitokotlin2.verify
import com.nhaarman.mockitokotlin2.whenever
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.newSingleThreadContext import kotlinx.coroutines.newSingleThreadContext
import kotlinx.coroutines.test.setMain import kotlinx.coroutines.test.setMain
@ -92,7 +96,6 @@ class SendViewModelTest {
verify(feedback).report(sendViewModel.metrics.values.first()) verify(feedback).report(sendViewModel.metrics.values.first())
} }
@Test @Test
fun testUpdateMetrics_mined() { fun testUpdateMetrics_mined() {
assertEquals(true, minedTx.isMined()) assertEquals(true, minedTx.isMined())
@ -106,5 +109,4 @@ class SendViewModelTest {
// Thread.sleep(100) // Thread.sleep(100)
// assertEquals(0, sendViewModel.metrics.size) // assertEquals(0, sendViewModel.metrics.size)
} }
}
}

View File

@ -4,22 +4,19 @@ buildscript {
repositories { repositories {
google() google()
jcenter() jcenter()
maven {
url 'https://maven.fabric.io/public'
}
} }
dependencies { dependencies {
classpath 'com.android.tools.build:gradle:4.1.3' classpath 'com.android.tools.build:gradle:4.1.3'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:${Deps.kotlinVersion}" classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:${Deps.kotlinVersion}"
classpath 'io.fabric.tools:gradle:1.31.2'
classpath 'com.bugsnag:bugsnag-android-gradle-plugin:4.7.5' classpath 'com.bugsnag:bugsnag-android-gradle-plugin:4.7.5'
classpath 'com.google.gms:google-services:4.3.5'
classpath 'com.google.firebase:firebase-crashlytics-gradle:2.5.2'
classpath "androidx.navigation:navigation-safe-args-gradle-plugin:${Deps.navigationVersion}" classpath "androidx.navigation:navigation-safe-args-gradle-plugin:${Deps.navigationVersion}"
} }
} }
allprojects { allprojects {
repositories { repositories {
// mavenLocal()
google() google()
mavenCentral() mavenCentral()
jcenter() jcenter()
@ -32,4 +29,3 @@ task clean(type: Delete) {
} }
defaultTasks 'clean', 'installZcashmainnetRelease' defaultTasks 'clean', 'installZcashmainnetRelease'

View File

@ -6,12 +6,10 @@ object Deps {
const val kotlinVersion = "1.4.32" const val kotlinVersion = "1.4.32"
const val navigationVersion = "2.3.0" const val navigationVersion = "2.3.0"
const val compileSdkVersion = 29 const val compileSdkVersion = 30
const val buildToolsVersion = "29.0.2" const val buildToolsVersion = "30.0.3"
const val minSdkVersion = 21 const val minSdkVersion = 21
const val targetSdkVersion = 29 const val targetSdkVersion = 30
const val versionName = "1.0.0-alpha69"
const val versionCode = 1_00_00_169 // last digits are alpha(0XX) beta(2XX) rc(4XX) release(8XX). Ex: 1_08_04_401 is an release candidate build of version 1.8.4 and 1_08_04_800 would be the final release.
const val packageName = "cash.z.ecc.android" const val packageName = "cash.z.ecc.android"

View File

@ -1,6 +1,6 @@
#Fri Apr 02 00:54:33 EDT 2021 #Fri Apr 02 00:54:33 EDT 2021
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionUrl=https\://services.gradle.org/distributions/gradle-6.8.2-bin.zip distributionUrl=https\://services.gradle.org/distributions/gradle-7.0-all.zip
distributionPath=wrapper/dists distributionPath=wrapper/dists
zipStorePath=wrapper/dists zipStorePath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME

21
ktlint.gradle Normal file
View File

@ -0,0 +1,21 @@
configurations {
ktlint
}
dependencies {
ktlint "com.pinterest:ktlint:0.41.0"
}
task ktlint(type: org.gradle.api.tasks.JavaExec, group: "verification") {
description = "Verifying Kotlin code style.."
classpath = configurations.ktlint
main = "com.pinterest.ktlint.Main"
args "-F", "src/**/*.kt", "--editorconfig=${rootProject.file(".editorconfig")}"
}
task ktlintFormat(type: org.gradle.api.tasks.JavaExec, group: "formatting") {
description = "Format Kotlin code style deviations."
classpath = configurations.ktlint
main = "com.pinterest.ktlint.Main"
args "-F", "src/**/*.kt", "--editorconfig=${rootProject.file(".editorconfig")}"
}