* 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

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

View File

@ -1,13 +1,13 @@
package cash.z.ecc.android
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 kotlinx.coroutines.delay
import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertEquals
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.Parameterized
@RunWith(AndroidJUnit4::class)
// @RunWith(Parameterized::class)

View File

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

View File

@ -4,7 +4,6 @@ import android.content.Context
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
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.type.ZcashNetwork
import okio.Buffer
@ -94,7 +93,6 @@ class IntegrationTest {
initializer.erase()
}
private fun ByteArray.toHex(): String {
val sb = StringBuilder(size * 2)
for (b in this)

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.sdk.ext.twig
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
class ZcashWalletApp : Application(), CameraXConfig.Provider {
@Inject
@ -102,10 +105,9 @@ class ZcashWalletApp : Application(), CameraXConfig.Provider {
}
}
fun ZcashWalletApp.isEmulator(): Boolean {
val goldfish = Build.HARDWARE.contains("goldfish");
val emu = (System.getProperty("ro.kernel.qemu", "")?.length ?: 0) > 0;
val goldfish = Build.HARDWARE.contains("goldfish")
val emu = (System.getProperty("ro.kernel.qemu", "")?.length ?: 0) > 0
val sdk = Build.MODEL.toLowerCase().contains("sdk")
return goldfish || emu || sdk;
return goldfish || emu || sdk
}

View File

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

View File

@ -8,6 +8,4 @@ import dagger.Module
includes = [ViewModelsActivityModule::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 {
return Synchronizer(initializer)
}
}

View File

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

View File

@ -1,6 +1,5 @@
package cash.z.ecc.android.di.module
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import cash.z.ecc.android.di.annotation.SynchronizerScope

View File

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

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.sdk.ext.Conversions
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.MathContext
import java.math.RoundingMode
import java.text.DecimalFormat
import java.text.NumberFormat
import java.util.*
import java.util.Locale
/**
* Do the necessary conversions in one place
@ -75,5 +72,4 @@ object WalletZecFormmatter {
BigDecimal(this ?: 0L, MathContext.DECIMAL128)
.divide(ConversionsUniform.ONE_ZEC_IN_ZATOSHI)
.setScale(LONG_SCALE, ConversionsUniform.roundingMode)
}

View File

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

View File

@ -55,7 +55,7 @@ inline fun EditText.limitDecimalPlaces(max: Int) {
// Restore the cursor position
if (oldText != textStr) {
val cursorPosition = editText.selectionEnd;
val cursorPosition = editText.selectionEnd
editText.setText(textStr)
editText.setSelection(
(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) {
}
})
}
fun TextView.convertZecToZatoshi(): Long? {
return try {
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.type.WalletBalance
import java.util.*
import java.util.Locale
import kotlin.math.roundToInt
/**

View File

@ -32,13 +32,12 @@ internal inline fun @receiver:StringRes Int.toAppStringFormatted(vararg formatAr
return ZcashWalletApp.instance.getString(this, *formatArgs)
}
/**
* Grab an integer from the application resources
*/
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

View File

@ -1,7 +1,9 @@
package cash.z.ecc.android.ext
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 kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.channelFlow
@ -40,8 +42,10 @@ fun View.onClickNavTo(navResId: Int, block: (() -> Any) = {}) {
setOnClickListener {
block()
(context as? MainActivity)?.safeNavigate(navResId)
?: throw IllegalStateException("Cannot navigate from this activity. " +
"Expected MainActivity but found ${context.javaClass.simpleName}")
?: throw IllegalStateException(
"Cannot navigate from this activity. " +
"Expected MainActivity but found ${context.javaClass.simpleName}"
)
}
}

View File

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

View File

@ -22,11 +22,13 @@ object Report {
// 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)
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),
"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),
"errorMessage" to (errorMessage ?: "None")
)
@ -241,6 +243,5 @@ class LaunchMetric private constructor(private val metric: Feedback.TimeMetric)
override fun toString(): String = metric.toString()
}
inline fun <T> Feedback.measure(type: Report.MetricType, block: () -> T): T =
this.measure(type.key, type.description, block)

View File

@ -85,7 +85,6 @@ import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch
import javax.inject.Inject
class MainActivity : AppCompatActivity() {
@Inject
@ -421,11 +420,14 @@ class MainActivity : AppCompatActivity() {
}
fun onFragmentBackPressed(fragment: Fragment, block: () -> Unit) {
onBackPressedDispatcher.addCallback(fragment, object : OnBackPressedCallback(true) {
onBackPressedDispatcher.addCallback(
fragment,
object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
block()
}
})
}
)
}
private fun showMessage(message: String, linger: Boolean = false) {
@ -534,7 +536,8 @@ class MainActivity : AppCompatActivity() {
if (dialog == null && !ignoreScanFailure) throttle("scanFailure", 20_000L) {
notified = true
runOnUiThread {
dialog = showScanFailure(error,
dialog = showScanFailure(
error,
onCancel = { dialog = null },
onDismiss = { dialog = null }
)
@ -564,7 +567,6 @@ class MainActivity : AppCompatActivity() {
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)
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
throttles[key] = noWork
findViewById<View>(android.R.id.content).postDelayed({
findViewById<View>(android.R.id.content).postDelayed(
{
throttles[key]?.let { pendingWork ->
throttles.remove(key)
if (pendingWork !== noWork) throttle(key, delay, pendingWork)
}
}, delay)
},
delay
)
}
/* Memo functions that might possibly get moved to MemoUtils */
// private val addressRegex = """zs\d\w{65,}""".toRegex()
suspend fun getSender(transaction: ConfirmedTransaction?): String {
if (transaction == null) return 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")
}
}
}

View File

@ -10,10 +10,15 @@ import androidx.lifecycle.lifecycleScope
import androidx.viewbinding.ViewBinding
import cash.z.ecc.android.feedback.Report
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.first
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
abstract class BaseFragment<T : ViewBinding> : Fragment() {
val mainActivity: MainActivity? get() = activity as MainActivity?

View File

@ -10,7 +10,12 @@ import androidx.recyclerview.widget.RecyclerView
import cash.z.ecc.android.R
import cash.z.ecc.android.databinding.FragmentHistoryBinding
import cash.z.ecc.android.di.viewmodel.activityViewModel
import cash.z.ecc.android.ext.*
import cash.z.ecc.android.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.Tap.HISTORY_BACK
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 kotlinx.coroutines.launch
class HistoryFragment : BaseFragment<FragmentHistoryBinding>() {
override val screen = Report.Screen.HISTORY
@ -34,7 +38,6 @@ class HistoryFragment : BaseFragment<FragmentHistoryBinding>() {
override fun inflate(inflater: LayoutInflater): FragmentHistoryBinding =
FragmentHistoryBinding.inflate(inflater)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
twig("HistoryFragment.onViewCreated")
super.onViewCreated(view, savedInstanceState)
@ -61,7 +64,7 @@ class HistoryFragment : BaseFragment<FragmentHistoryBinding>() {
goneIf(change <= 0L)
val changeString = WalletZecFormmatter.toZecStringFull(change)
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() {
twig("scrolling to the top")
binding.recyclerTransactions.apply {
postDelayed({
postDelayed(
{
smoothScrollToPosition(0)
}, 5L)
},
5L
)
}
}

View File

@ -68,7 +68,6 @@ class HistoryViewModel @Inject constructor() : ViewModel() {
var txId: String? = null
)
private suspend fun ConfirmedTransaction?.toUiModel(latestHeight: Int? = null): UiModel = UiModel().apply {
this@toUiModel.let { tx ->
txId = toTxId(tx?.rawTransactionId)
@ -105,7 +104,8 @@ class HistoryViewModel @Inject constructor() : ViewModel() {
if (it.minedHeight > 0 && hasLatestHeight) {
val confirmations = latestHeight!! - it.minedHeight + 1
confirmation = if (confirmations >= 10) getString(R.string.transaction_status_confirmed) else "$confirmations ${getString(
R.string.transaction_status_confirming)}"
R.string.transaction_status_confirming
)}"
} else {
if (!hasLatestHeight && isSufficientlyOld(tx)) {
twig("Warning: could not load latestheight from server to determine confirmations but this transaction is mined and old enough to be considered confirmed")
@ -118,11 +118,8 @@ class HistoryViewModel @Inject constructor() : ViewModel() {
} else {
confirmation = getString(R.string.transaction_status_pending)
}
}
// val mainActivity = (context as MainActivity)
// inbound v. outbound values
when (isInbound) {
true -> {
topLabel = getString(R.string.transaction_story_inbound)
@ -166,8 +163,7 @@ class HistoryViewModel @Inject constructor() : ViewModel() {
private fun isSufficientlyOld(tx: ConfirmedTransaction): Boolean {
val threshold = 75 * 1000 * 25 // approx 25 blocks
val delta = System.currentTimeMillis() / 1000L - tx.blockTimeInSeconds
return tx.minedHeight > synchronizer.network.saplingActivationHeight
&& delta < threshold
return tx.minedHeight > synchronizer.network.saplingActivationHeight &&
delta < threshold
}
}

View File

@ -13,9 +13,9 @@ class TransactionAdapter<T : ConfirmedTransaction> :
override fun areItemsTheSame(
oldItem: 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
&& ((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(
oldItem: T,

View File

@ -4,32 +4,34 @@ import android.content.res.ColorStateList
import android.graphics.ColorMatrix
import android.graphics.ColorMatrixColorFilter
import android.os.Bundle
import android.text.format.DateUtils
import android.text.method.ScrollingMovementMethod
import android.view.LayoutInflater
import android.view.View
import androidx.core.view.ViewCompat
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.ZcashWalletApp
import cash.z.ecc.android.databinding.FragmentTransactionBinding
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.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.twig
import cash.z.ecc.android.ui.MainActivity
import cash.z.ecc.android.ui.base.BaseFragment
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.stateIn
import kotlinx.coroutines.launch
import java.util.*
class TransactionFragment : BaseFragment<FragmentTransactionBinding>() {
override val screen = Report.Screen.TRANSACTION
@ -117,7 +119,6 @@ class TransactionFragment : BaseFragment<FragmentTransactionBinding>() {
uiModel.toAddressLabel()?.let { subwaySpotAddress.visible(); subwayLabelAddress.visible(); subwayLabelAddress.text = it }
uiModel.toAddressClickListener()?.let { subwayLabelAddress.setOnClickListener(it) }
// TODO: remove logic from sections below and add more fields or extension functions to UiModel
uiModel.confirmation?.let {
subwaySpotConfirmations.visible(); subwayLabelConfirmations.visible()
@ -196,12 +197,4 @@ class TransactionFragment : BaseFragment<FragmentTransactionBinding>() {
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.widget.ImageView
import android.widget.TextView
import androidx.annotation.ColorRes
import androidx.annotation.IntegerRes
import androidx.annotation.StringRes
import androidx.appcompat.content.res.AppCompatResources
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.RecyclerView
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.toAbbreviatedAddress
import cash.z.ecc.android.ui.MainActivity
import cash.z.ecc.android.ui.util.MemoUtil
import cash.z.ecc.android.ui.util.toUtf8Memo
import kotlinx.coroutines.launch
import java.text.SimpleDateFormat
class TransactionViewHolder<T : ConfirmedTransaction>(itemView: View) : RecyclerView.ViewHolder(itemView) {
private val indicator = itemView.findViewById<View>(R.id.indicator)
private val amountText = itemView.findViewById<TextView>(R.id.text_transaction_amount)
private val topText = itemView.findViewById<TextView>(R.id.text_transaction_top)
@ -132,6 +129,7 @@ class TransactionViewHolder<T : ConfirmedTransaction>(itemView: View) : Recycler
indicator.setBackgroundColor(indicatorBackground.toAppColor())
transactionArrow.setColorFilter(arrowBackgroundTint.toAppColor())
transactionArrow.rotation = arrowRotation.toAppInt().toFloat()
var bottomTextRightDrawable: Drawable? = null
iconMemo.goneIf(!transaction?.memo.toUtf8Memo().isNotEmpty())
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)
}

View File

@ -8,7 +8,6 @@ import android.view.View
import androidx.recyclerview.widget.RecyclerView
import cash.z.ecc.android.R
class TransactionsFooter(context: Context) : RecyclerView.ItemDecoration() {
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 =
FragmentHomeBinding.inflate(inflater)
//
// 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
onModelUpdated(null, uiModel.copy(pendingSend = WalletZecFormmatter.toZecStringFull(sendViewModel.zatoshiAmount.coerceAtLeast(0))))
}
}
private fun onClearAmount() {
@ -190,7 +188,6 @@ class HomeFragment : BaseFragment<FragmentHomeBinding>() {
}
}
//
// Public UI API
//
@ -294,7 +291,6 @@ class HomeFragment : BaseFragment<FragmentHomeBinding>() {
}
}
//
// Private UI Events
//
@ -413,7 +409,6 @@ class HomeFragment : BaseFragment<FragmentHomeBinding>() {
}.launchIn(resumedScope)
}
//
// Inner classes and extensions
//
@ -444,7 +439,6 @@ class HomeFragment : BaseFragment<FragmentHomeBinding>() {
return this
}
//
// User Interruptions
//

View File

@ -5,14 +5,23 @@ import cash.z.ecc.android.R
import cash.z.ecc.android.ext.toAppString
import cash.z.ecc.android.sdk.SdkSynchronizer
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.exception.RustLayerException
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.twig
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 kotlin.math.roundToInt
@ -44,8 +53,8 @@ class HomeViewModel @Inject constructor() : ViewModel() {
val zec = typedChars.scan(preTypedChars) { acc, c ->
when {
// no-op cases
acc == "0" && c == '0'
|| (c == backspace && acc == "0")
acc == "0" && c == '0' ||
(c == backspace && acc == "0")
|| (c == decimal && acc.contains(decimal)) -> {
acc
}

View File

@ -1,7 +1,6 @@
package cash.z.ecc.android.ui.home
import android.animation.ValueAnimator
import cash.z.ecc.android.sdk.ext.twig
import com.airbnb.lottie.LottieAnimationView
class MagicSnakeLoader(
@ -54,14 +53,17 @@ class MagicSnakeLoader(
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) {
lottie.resumeAnimation()
isPaused = false
isStarted = true
}
}, 200L)
},
200L
)
}
private val isDownloading get() = downloadProgress in 1..99
@ -151,4 +153,3 @@ class MagicSnakeLoader(
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_SHIELD
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.isCancelled
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.launch
class AwesomeFragment : BaseFragment<FragmentAwesomeBinding>() {
override val screen = Report.Screen.AWESOME
@ -96,7 +94,6 @@ class AwesomeFragment : BaseFragment<FragmentAwesomeBinding>() {
viewModel.getTransparentBalance().let { balance ->
onBalanceUpdated(balance, utxoCount)
}
}
private fun onAddressLoaded(address: String) {
@ -200,7 +197,6 @@ class AwesomeFragment : BaseFragment<FragmentAwesomeBinding>() {
}
}
private fun onShieldComplete(isSuccess: Boolean) {
binding.lottieShielding.visibility = View.GONE
@ -248,9 +244,6 @@ class AwesomeFragment : BaseFragment<FragmentAwesomeBinding>() {
}
}
private fun PendingTransaction.toUiModel() = UiModel().also { model ->
when {
isCancelled() -> {

View File

@ -3,21 +3,16 @@ package cash.z.ecc.android.ui.profile
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewTreeObserver
import android.view.WindowManager
import android.widget.Toast
import androidx.core.view.doOnLayout
import androidx.navigation.fragment.navArgs
import cash.z.ecc.android.R
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.Funnel.UserFeedback
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.ui.base.BaseFragment
/**
* Fragment representing the home screen of the app. This is the screen most often seen by the user when launching the
* application.
@ -68,7 +63,6 @@ class FeedbackFragment : BaseFragment<FragmentFeedbackBinding>() {
}
}
//
// Private API
//

View File

@ -40,7 +40,6 @@ import cash.z.ecc.android.ui.util.DebugFileTwig
import kotlinx.coroutines.launch
import java.io.File
class ProfileFragment : BaseFragment<FragmentProfileBinding>() {
override val screen = Report.Screen.PROFILE
@ -93,13 +92,14 @@ class ProfileFragment : BaseFragment<FragmentProfileBinding>() {
if (viewModel.isEasterEggTriggered()) {
binding.iconProfile.setImageResource(R.drawable.ic_profile_zebra_02)
}
}
private fun onEnterAwesomeMode() {
(context as? MainActivity)?.safeNavigate(R.id.action_nav_profile_to_nav_awesome)
?: throw IllegalStateException("Cannot navigate from this activity. " +
"Expected MainActivity but found ${context?.javaClass?.simpleName}")
?: throw IllegalStateException(
"Cannot navigate from this activity. " +
"Expected MainActivity but found ${context?.javaClass?.simpleName}"
)
}
override fun onResume() {

View File

@ -6,7 +6,6 @@ import cash.z.ecc.android.ZcashWalletApp
import cash.z.ecc.android.ext.Const
import cash.z.ecc.android.lockbox.LockBox
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.db.entity.PendingTransaction
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 kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import javax.inject.Inject
import javax.inject.Named

View File

@ -8,20 +8,16 @@ import android.view.LayoutInflater
import android.view.View
import android.widget.TextView
import cash.z.android.qrecycler.QRecycler
import cash.z.ecc.android.R
import cash.z.ecc.android.databinding.FragmentReceiveNewBinding
import cash.z.ecc.android.di.viewmodel.viewModel
import cash.z.ecc.android.ext.distribute
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.Tap.*
import cash.z.ecc.android.ui.base.BaseFragment
import cash.z.ecc.android.sdk.ext.toAbbreviatedAddress
import cash.z.ecc.android.feedback.Report.Tap.RECEIVE_BACK
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 kotlinx.coroutines.launch
import kotlin.math.roundToInt
class ReceiveFragment : BaseFragment<FragmentReceiveNewBinding>() {
override val screen = Report.Screen.RECEIVE

View File

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

View File

@ -6,7 +6,11 @@ import android.os.Bundle
import android.util.DisplayMetrics
import android.view.LayoutInflater
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.core.content.ContextCompat
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.feedback.Report
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.ui.base.BaseFragment
import cash.z.ecc.android.ui.send.SendViewModel
@ -26,7 +29,9 @@ import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors
class ScanFragment : BaseFragment<FragmentScanBinding>() {
override val screen = Report.Screen.SCAN
private val viewModel: ScanViewModel by viewModel()
private val sendViewModel: SendViewModel by activityViewModel()
@ -54,9 +59,12 @@ class ScanFragment : BaseFragment<FragmentScanBinding>() {
override fun onAttach(context: Context) {
super.onAttach(context)
cameraProviderFuture = ProcessCameraProvider.getInstance(context)
cameraProviderFuture.addListener(Runnable {
cameraProviderFuture.addListener(
Runnable {
bindPreview(cameraProviderFuture.get())
}, ContextCompat.getMainExecutor(context))
},
ContextCompat.getMainExecutor(context)
)
}
override fun onDestroyView() {
@ -87,9 +95,12 @@ class ScanFragment : BaseFragment<FragmentScanBinding>() {
.setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
.build()
imageAnalysis.setAnalyzer(cameraExecutor!!, QrAnalyzer { q, i ->
imageAnalysis.setAnalyzer(
cameraExecutor!!,
QrAnalyzer { q, i ->
onQrScanned(q, i)
})
}
)
// Must unbind the use-cases before rebinding them
cameraProvider.unbindAll()
@ -102,7 +113,6 @@ class ScanFragment : BaseFragment<FragmentScanBinding>() {
mainActivity?.feedback?.report(t)
twig("Error while opening the camera: $t")
}
}
/**
@ -114,7 +124,8 @@ class ScanFragment : BaseFragment<FragmentScanBinding>() {
height
)
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_16_9
@ -157,12 +168,6 @@ class ScanFragment : BaseFragment<FragmentScanBinding>() {
// overlay.set(list)
// }
//
// Permissions
//
@ -171,7 +176,7 @@ class ScanFragment : BaseFragment<FragmentScanBinding>() {
get() {
return try {
val info = mainActivity?.packageManager
?.getPackageInfo(mainActivity?.packageName, PackageManager.GET_PERMISSIONS)
?.getPackageInfo(mainActivity?.packageName ?: "", PackageManager.GET_PERMISSIONS)
val ps = info?.requestedPermissions
if (ps != null && ps.isNotEmpty()) {
ps

View File

@ -27,5 +27,4 @@ class ScanViewModel @Inject constructor() : ViewModel() {
super.onCleared()
twig("${javaClass.simpleName} cleared!")
}
}

View File

@ -4,18 +4,15 @@ import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import androidx.lifecycle.lifecycleScope
import cash.z.ecc.android.R
import cash.z.ecc.android.databinding.FragmentSendConfirmBinding
import cash.z.ecc.android.di.viewmodel.activityViewModel
import cash.z.ecc.android.ext.WalletZecFormmatter
import cash.z.ecc.android.ext.goneIf
import cash.z.ecc.android.ext.onClickNavTo
import cash.z.ecc.android.feedback.Report
import cash.z.ecc.android.feedback.Report.Funnel.Send
import cash.z.ecc.android.feedback.Report.Tap.*
import cash.z.ecc.android.ui.base.BaseFragment
import cash.z.ecc.android.feedback.Report.Tap.SEND_CONFIRM_NEXT
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
class SendConfirmFragment : BaseFragment<FragmentSendConfirmBinding>() {

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.feedback.Report
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.twig
import cash.z.ecc.android.ui.base.BaseFragment
@ -128,7 +133,8 @@ class SendFinalFragment : BaseFragment<FragmentSendFinalBinding>() {
isFailure() -> {
model.title = getString(R.string.send_final_button_primary_failed)
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.primaryButtonText = getString(R.string.send_final_button_primary_retry)
model.primaryAction = { onReturnToSend() }
@ -158,5 +164,4 @@ class SendFinalFragment : BaseFragment<FragmentSendFinalBinding>() {
var primaryButtonText: String = "See Details",
var primaryAction: () -> Unit = {}
)
}

View File

@ -19,10 +19,24 @@ import androidx.lifecycle.viewModelScope
import cash.z.ecc.android.R
import cash.z.ecc.android.databinding.FragmentSendBinding
import cash.z.ecc.android.di.viewmodel.activityViewModel
import cash.z.ecc.android.ext.*
import cash.z.ecc.android.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.Tap.*
import cash.z.ecc.android.sdk.ext.*
import cash.z.ecc.android.feedback.Report.Tap.SEND_ADDRESS_BACK
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.WalletBalance
import cash.z.ecc.android.ui.base.BaseFragment
@ -30,7 +44,8 @@ import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
class SendFragment : BaseFragment<FragmentSendBinding>(),
class SendFragment :
BaseFragment<FragmentSendBinding>(),
ClipboardManager.OnPrimaryClipChangedListener {
override val screen = Report.Screen.SEND_ADDRESS
@ -49,7 +64,6 @@ class SendFragment : BaseFragment<FragmentSendBinding>(),
applyViewModel(sendViewModel)
updateAddressUi(false)
// Apply behaviors
binding.buttonSend.setOnClickListener {
@ -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) {
if (isMemoHidden) {
binding.textLayoutMemo.gone()
@ -186,7 +199,6 @@ class SendFragment : BaseFragment<FragmentSendBinding>(),
}
}
private fun onSubmit(unused: EditText? = null) {
sendViewModel.toAddress = binding.inputZcashAddress.text.toString()
sendViewModel.validate(requireContext(), availableZatoshi, maxZatoshi).onFirstWith(resumedScope) { errorMessage ->
@ -217,7 +229,6 @@ class SendFragment : BaseFragment<FragmentSendBinding>(),
}
}
override fun onAttach(context: Context) {
super.onAttach(context)
mainActivity?.clipboard?.addPrimaryClipChangedListener(this)
@ -280,7 +291,8 @@ class SendFragment : BaseFragment<FragmentSendBinding>(),
imageLastUsedShield,
lastUsedAddressLabel,
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)
}
@ -352,7 +364,6 @@ class SendFragment : BaseFragment<FragmentSendBinding>(),
return lastUsedAddress
}
private fun ClipboardManager.text(): CharSequence =
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.feedback.Report
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.util.INCLUDE_MEMO_PREFIX_STANDARD

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.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.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.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.twig
import cash.z.ecc.android.sdk.tool.DerivationTool
@ -140,7 +150,6 @@ class SendViewModel @Inject constructor() : ViewModel() {
includeFromAddress = false
}
//
// Analytics
//
@ -176,7 +185,7 @@ class SendViewModel @Inject constructor() : ViewModel() {
}
}
private fun updateMetrics(tx: PendingTransaction) {
fun updateMetrics(tx: PendingTransaction) {
try {
when {
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 ->
metric.takeUnless { (it.elapsedTime ?: 0) <= 0L }?.let {
viewModelScope.launch {
@ -236,18 +245,9 @@ class SendViewModel @Inject constructor() : ViewModel() {
}
}
}
}
private fun Keyed<String>.toMetricIdFor(id: Long): String = "$id.$key"
private fun String.toRelatedMetricId(): String = "$this.related"
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.databinding.FragmentSettingsBinding
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.ext.collectWith
import cash.z.ecc.android.sdk.ext.twig
@ -53,7 +59,6 @@ class SettingsFragment : BaseFragment<FragmentSettingsBinding>() {
viewModel.uiModels.collectWith(resumedScope, ::onUiModelUpdated)
}
//
// Event handlers
//
@ -137,7 +142,6 @@ class SettingsFragment : BaseFragment<FragmentSettingsBinding>() {
context?.showUpdateServerCriticalError(message)
}
//
// Utilities
//
@ -151,4 +155,3 @@ class SettingsFragment : BaseFragment<FragmentSettingsBinding>() {
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.ext.twig
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.cancellable
import javax.inject.Inject
import javax.inject.Named
import kotlin.properties.Delegates.observable
@ -28,7 +27,6 @@ class SettingsViewModel @Inject constructor() : ViewModel() {
var pendingHost by observable("", ::onUpdateModel)
var pendingPortText by observable("", ::onUpdateModel)
private fun getHost(): String {
return prefs[Const.Pref.SERVER_HOST] ?: Const.Default.Server.HOST
}

View File

@ -100,9 +100,12 @@ class LandingFragment : BaseFragment<FragmentLandingBinding>() {
override fun onResume() {
super.onResume()
view?.postDelayed({
view?.postDelayed(
{
mainActivity?.hideKeyboard()
}, 25L)
},
25L
)
}
private fun onSkip(count: Int) {

View File

@ -10,7 +10,6 @@ import android.view.MotionEvent
import android.view.MotionEvent.ACTION_DOWN
import android.view.MotionEvent.ACTION_UP
import android.view.View
import android.widget.RelativeLayout
import android.widget.TextView
import androidx.lifecycle.lifecycleScope
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_DONE
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 com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.tylersuehr.chips.Chip
@ -36,7 +34,6 @@ import com.tylersuehr.chips.ChipsAdapter
import com.tylersuehr.chips.SeedWordAdapter
import kotlinx.coroutines.launch
class RestoreFragment : BaseFragment<FragmentRestoreBinding>(), View.OnKeyListener {
override val screen = Report.Screen.RESTORE
@ -57,7 +54,6 @@ class RestoreFragment : BaseFragment<FragmentRestoreBinding>(), View.OnKeyListen
}.also { onChipsModified() }
seedWordRecycler.adapter = seedWordAdapter
binding.chipsInput.apply {
setFilterableChipList(getChips())
setDelimiter("[ ;,]", true)
@ -116,7 +112,6 @@ class RestoreFragment : BaseFragment<FragmentRestoreBinding>(), View.OnKeyListen
touchScreenForUser()
}
private fun onExit() {
mainActivity?.reportFunnel(Restore.Exit)
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
// sometimes closes randomly and inexplicably in between seed word entries
private fun forceShowKeyboard() {
requireView().postDelayed({
requireView().postDelayed(
{
val isDone = (seedWordAdapter?.itemCount ?: 0) > 24
val focusedView = if (isDone) binding.inputBirthdate else seedWordAdapter!!.editText
mainActivity!!.showKeyboard(focusedView)
focusedView.requestFocus()
}, 500L)
},
500L
)
}
private fun reportWords(count: Int) {
@ -220,11 +218,14 @@ class RestoreFragment : BaseFragment<FragmentRestoreBinding>(), View.OnKeyListen
private fun touchScreenForUser() {
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))
dispatchTouchEvent(motionEvent(ACTION_UP))
}, 100L)
},
100L
)
}
}
@ -235,7 +236,6 @@ class RestoreFragment : BaseFragment<FragmentRestoreBinding>(), View.OnKeyListen
override fun onKey(v: View?, keyCode: Int, event: KeyEvent?): Boolean {
return false
}
}
class SeedWordChip(val word: String, var index: Int = -1) : Chip() {

View File

@ -7,7 +7,6 @@ import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
import cash.z.ecc.android.R
import cash.z.ecc.android.ext.toAppColor
import cash.z.ecc.android.sdk.ext.twig
import cash.z.ecc.android.ui.setup.SeedWordChip
class SeedWordAdapter : ChipsAdapter {
@ -24,7 +23,7 @@ class SeedWordAdapter : ChipsAdapter {
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
if (getItemViewType(position) == CHIP) { // Chips
// 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 {
val size = mDataSource.selectedChips.size
@ -66,10 +65,13 @@ class SeedWordAdapter : ChipsAdapter {
if (mDataSource.originalChips.firstOrNull { it.title == text } != null) {
mDataSource.addSelectedChip(DefaultCustomChip(text))
mEditText.apply {
postDelayed({
postDelayed(
{
setText("")
requestFocus()
}, 50L)
},
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) =
WalletBirthdayTool.loadNearest(ZcashWalletApp.instance, network, birthdayHeight)
//
// Storage Helpers
//
@ -229,5 +228,4 @@ class WalletSetupViewModel @Inject constructor() : ViewModel() {
lockBox[Const.Backup.VIEWING_KEY] = vk.extfvk
lockBox[Const.Backup.PUBLIC_KEY] = vk.extpub
}
}

View File

@ -1,12 +1,13 @@
package cash.z.ecc.android
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 org.junit.Assert.assertTrue
import org.junit.Test
import kotlin.math.round
import kotlin.math.roundToInt
class ScratchPad {
@ -23,7 +24,7 @@ class ScratchPad {
t0 = t
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 flow3 = flowOf("A", "B").onEach { delay(450); println("$Δt\temitting $it") }
@ -50,5 +51,4 @@ class ScratchPad {
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.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 com.nhaarman.mockitokotlin2.*
import com.nhaarman.mockitokotlin2.verify
import com.nhaarman.mockitokotlin2.whenever
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.newSingleThreadContext
import kotlinx.coroutines.test.setMain
@ -92,7 +96,6 @@ class SendViewModelTest {
verify(feedback).report(sendViewModel.metrics.values.first())
}
@Test
fun testUpdateMetrics_mined() {
assertEquals(true, minedTx.isMined())
@ -106,5 +109,4 @@ class SendViewModelTest {
// Thread.sleep(100)
// assertEquals(0, sendViewModel.metrics.size)
}
}

View File

@ -4,22 +4,19 @@ buildscript {
repositories {
google()
jcenter()
maven {
url 'https://maven.fabric.io/public'
}
}
dependencies {
classpath 'com.android.tools.build:gradle:4.1.3'
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.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}"
}
}
allprojects {
repositories {
// mavenLocal()
google()
mavenCentral()
jcenter()
@ -32,4 +29,3 @@ task clean(type: Delete) {
}
defaultTasks 'clean', 'installZcashmainnetRelease'

View File

@ -6,12 +6,10 @@ object Deps {
const val kotlinVersion = "1.4.32"
const val navigationVersion = "2.3.0"
const val compileSdkVersion = 29
const val buildToolsVersion = "29.0.2"
const val compileSdkVersion = 30
const val buildToolsVersion = "30.0.3"
const val minSdkVersion = 21
const val targetSdkVersion = 29
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 targetSdkVersion = 30
const val packageName = "cash.z.ecc.android"

View File

@ -1,6 +1,6 @@
#Fri Apr 02 00:54:33 EDT 2021
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
zipStorePath=wrapper/dists
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")}"
}