* 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,16 +1,16 @@
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)
// @RunWith(Parameterized::class)
class MemoTest(val input: String, val output: String) {
@Test

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
@ -24,7 +23,7 @@ class IntegrationTest {
private val mnemonics = Mnemonics()
private val phrase =
"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
fun start() {
@ -37,7 +36,7 @@ class IntegrationTest {
assertEquals(
"Generated incorrect BIP-39 seed!",
"f4e3d38d9c244da7d0407e19a93c80429614ee82dcf62c141235751c9f1228905d12a1f275f" +
"5c22f6fb7fcd9e0a97f1676e0eec53fdeeeafe8ce8aa39639b9fe",
"5c22f6fb7fcd9e0a97f1676e0eec53fdeeeafe8ce8aa39639b9fe",
seed.toHex()
)
}
@ -72,10 +71,10 @@ class IntegrationTest {
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(
"LockBox does not support the maximum length seed phrase." +
" Expected: $maxSeedPhraseLength but was: $acceptedSize",
" Expected: $maxSeedPhraseLength but was: $acceptedSize",
acceptedSize > maxSeedPhraseLength
)
}
@ -94,7 +93,6 @@ class IntegrationTest {
initializer.erase()
}
private fun ByteArray.toHex(): String {
val sb = StringBuilder(size * 2)
for (b in this)

View File

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

@ -13,7 +13,7 @@ class ViewModelFactory @Inject constructor(
modelClass.isAssignableFrom(it.key)
}?.value ?: throw IllegalArgumentException(
"No map entry found for ${modelClass.canonicalName}. Verify that this ViewModel has" +
" been added to the ViewModelModule. ${creators.keys}"
" been added to the ViewModelModule. ${creators.keys}"
)
@Suppress("UNCHECKED_CAST")
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.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 = {
// do not let the user back into the app because we cannot recover from this case
showUninitializedError(error, onDismiss)
})
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

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

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}"
)
}
}
@ -51,7 +55,7 @@ fun View.onClickNavUp(block: (() -> Any) = {}) {
(context as? MainActivity)?.navController?.navigateUp()
?: throw IllegalStateException(
"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()
?: throw IllegalStateException(
"Cannot navigate from this activity. " +
"Expected MainActivity but found ${context.javaClass.simpleName}"
"Expected MainActivity but found ${context.javaClass.simpleName}"
)
}
}

View File

@ -58,7 +58,7 @@ class FeedbackBugsnag : FeedbackCoordinator.FeedbackObserver {
private class ReorgException(errorHeight: Int, rewindHeight: Int, reorgMesssage: String) :
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"
)
}

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")
)
@ -82,7 +84,7 @@ object Report {
*properties
) {
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)
}
@ -94,7 +96,7 @@ object Report {
*properties
) {
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
object SelfSend : Issue("self.send")
@ -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
@ -224,9 +223,9 @@ class MainActivity : AppCompatActivity() {
} catch (t: Throwable) {
twig(
"WARNING: during callback, did not navigate to destination: R.id.${
resources.getResourceEntryName(
destination
)
resources.getResourceEntryName(
destination
)
} due to: $t"
)
}
@ -237,9 +236,9 @@ class MainActivity : AppCompatActivity() {
} catch (t: Throwable) {
twig(
"WARNING: did not immediately navigate to destination: R.id.${
resources.getResourceEntryName(
destination
)
resources.getResourceEntryName(
destination
)
} due to: $t"
)
}
@ -310,7 +309,7 @@ class MainActivity : AppCompatActivity() {
}
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) {
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()
@ -348,7 +347,7 @@ class MainActivity : AppCompatActivity() {
ERROR_TIMEOUT -> doNothing("Oops. It timed out.")
ERROR_UNABLE_TO_PROCESS -> doNothing(".")
ERROR_VENDOR -> doNothing("We got some weird error and you should report this.")
else -> {
else -> {
twig("Warning: unrecognized authentication error $errorCode")
doNothing("Authentication failed with error code $errorCode")
}
@ -417,15 +416,18 @@ class MainActivity : AppCompatActivity() {
}
fun preventBackPress(fragment: Fragment) {
onFragmentBackPressed(fragment){}
onFragmentBackPressed(fragment) {}
}
fun onFragmentBackPressed(fragment: Fragment, block: () -> Unit) {
onBackPressedDispatcher.addCallback(fragment, object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
block()
onBackPressedDispatcher.addCallback(
fragment,
object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
block()
}
}
})
)
}
private fun showMessage(message: String, linger: Boolean = false) {
@ -440,26 +442,26 @@ class MainActivity : AppCompatActivity() {
.make(view, "$message", Snackbar.LENGTH_INDEFINITE)
.setAction(action) { /*auto-close*/ }
val snackBarView = snacks.view as ViewGroup
val navigationBarHeight = resources.getDimensionPixelSize(
resources.getIdentifier(
"navigation_bar_height",
"dimen",
"android"
)
)
val params = snackBarView.getChildAt(0).layoutParams as ViewGroup.MarginLayoutParams
params.setMargins(
params.leftMargin,
params.topMargin,
params.rightMargin,
navigationBarHeight
val snackBarView = snacks.view as ViewGroup
val navigationBarHeight = resources.getDimensionPixelSize(
resources.getIdentifier(
"navigation_bar_height",
"dimen",
"android"
)
)
val params = snackBarView.getChildAt(0).layoutParams as ViewGroup.MarginLayoutParams
params.setMargins(
params.leftMargin,
params.topMargin,
params.rightMargin,
navigationBarHeight
)
snackBarView.getChildAt(0).setLayoutParams(params)
snackBarView.getChildAt(0).setLayoutParams(params)
snacks
} else {
snackbar!!.setText(message).setAction(action) {/*auto-close*/}
snackbar!!.setText(message).setAction(action) { /*auto-close*/ }
}.also {
if (!it.isShownOrQueued) it.show()
}
@ -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({
throttles[key]?.let { pendingWork ->
throttles.remove(key)
if (pendingWork !== noWork) throttle(key, delay, pendingWork)
}
}, delay)
findViewById<View>(android.R.id.content).postDelayed(
{
throttles[key]?.let { pendingWork ->
throttles.remove(key)
if (pendingWork !== noWork) throttle(key, delay, pendingWork)
}
},
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({
smoothScrollToPosition(0)
}, 5L)
postDelayed(
{
smoothScrollToPosition(0)
},
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)
@ -154,7 +151,7 @@ class HistoryViewModel @Inject constructor() : ViewModel() {
private fun toTxId(tx: ByteArray?): String? {
if (tx == null) return null
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]))
}
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
// information suggests that the TX is confirmed. We can improve this, later.
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
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
// 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.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!!)))
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
@ -53,7 +55,7 @@ class TransactionFragment : BaseFragment<FragmentTransactionBinding>() {
// sharedElementReturnTransition = ChangeBounds().apply { duration = 1500 }
// enterTransition = Fade().apply {
// 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.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()
@ -162,7 +163,7 @@ class TransactionFragment : BaseFragment<FragmentTransactionBinding>() {
binding.subwaySpotMemoContent.rotation = 90.0f
} else {
binding.subwayLabelMemo.setText(getString(R.string.transaction_with_memo))
binding.subwayLabelMemo.scrollTo(0,0)
binding.subwayLabelMemo.scrollTo(0, 0)
binding.subwayLabelMemo.invalidate()
twig("setting memo text to: with a memo")
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)
}
}
}

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)
@ -40,7 +37,7 @@ class TransactionViewHolder<T : ConfirmedTransaction>(itemView: View) : Recycler
val mainActivity = itemView.context as MainActivity
mainActivity.lifecycleScope.launch {
// update view
var lineOne:CharSequence = ""
var lineOne: CharSequence = ""
var lineTwo = ""
var amountZec = ""
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()}"
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?)
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"
if (isMined) {
arrowRotation = R.integer.transaction_arrow_rotation_send
@ -132,7 +129,8 @@ class TransactionViewHolder<T : ConfirmedTransaction>(itemView: View) : Recycler
indicator.setBackgroundColor(indicatorBackground.toAppColor())
transactionArrow.setColorFilter(arrowBackgroundTint.toAppColor())
transactionArrow.rotation = arrowRotation.toAppInt().toFloat()
var bottomTextRightDrawable:Drawable? = null
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

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

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
//
@ -277,7 +274,7 @@ class HomeFragment : BaseFragment<FragmentHomeBinding>() {
val change = WalletZecFormmatter.toZecStringFull(totalBalance - availableBalance)
"(${getString(R.string.home_banner_expecting)} +$change ZEC)".toColoredSpan(R.color.text_light, "+$change")
} 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
//
@ -320,9 +316,9 @@ class HomeFragment : BaseFragment<FragmentHomeBinding>() {
else -> {
buildString {
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) {
append ("${maybeComma()}processorInfo=ProcessorInfo(")
append("${maybeComma()}processorInfo=ProcessorInfo(")
val startLength = length
fun innerComma() = if (length > startLength) ", " else ""
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}")
append(")")
}
if (old.availableBalance != new.availableBalance) append ("${maybeComma()}availableBalance=${new.availableBalance}")
if (old.totalBalance != new.totalBalance) append ("${maybeComma()}totalBalance=${new.totalBalance}")
if (old.pendingSend != new.pendingSend) append ("${maybeComma()}pendingSend=${new.pendingSend}")
if (old.availableBalance != new.availableBalance) append("${maybeComma()}availableBalance=${new.availableBalance}")
if (old.totalBalance != new.totalBalance) append("${maybeComma()}totalBalance=${new.totalBalance}")
if (old.pendingSend != new.pendingSend) append("${maybeComma()}pendingSend=${new.pendingSend}")
append(")")
}
}
@ -413,7 +409,6 @@ class HomeFragment : BaseFragment<FragmentHomeBinding>() {
}.launchIn(resumedScope)
}
//
// Inner classes and extensions
//
@ -444,11 +439,10 @@ class HomeFragment : BaseFragment<FragmentHomeBinding>() {
return this
}
//
// 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.
var hasInterrupted = false
private fun canInterruptUser(): Boolean {

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
@ -39,14 +48,14 @@ class HomeViewModel @Inject constructor() : ViewModel() {
}
_typedChars = ConflatedBroadcastChannel()
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 zec = typedChars.scan(preTypedChars) { acc, c ->
when {
// no-op cases
acc == "0" && c == '0'
|| (c == backspace && acc == "0")
|| (c == decimal && acc.contains(decimal)) -> {
acc == "0" && c == '0' ||
(c == backspace && acc == "0")
|| (c == decimal && acc.contains(decimal)) -> {
acc
}
c == backspace && acc.length <= 1 -> {
@ -68,9 +77,9 @@ class HomeViewModel @Inject constructor() : ViewModel() {
}
twig("initializing view models stream")
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)
}.onStart{ emit(UiModel()) }
}.onStart { emit(UiModel()) }
}.conflate()
}

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({
// 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)
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
)
}
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) {
// don't hardcode the progress until the loop animation has completed, cleanly
if (isPaused) {
@ -117,7 +119,7 @@ class MagicSnakeLoader(
}
private fun removeLoops() {
lottie.frame.let {frame ->
lottie.frame.let { frame ->
if (frame in 33..67) {
lottie.frame = frame + 34
} else if (frame in 0..33) {
@ -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) {
@ -119,7 +116,7 @@ class AwesomeFragment : BaseFragment<FragmentAwesomeBinding>() {
1 -> binding.textAddressPart2
else -> throw IllegalArgumentException(
"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) {
binding.lottieShielding.visibility = View.GONE
@ -248,9 +244,6 @@ class AwesomeFragment : BaseFragment<FragmentAwesomeBinding>() {
}
}
private fun PendingTransaction.toUiModel() = UiModel().also { model ->
when {
isCancelled() -> {
@ -283,7 +276,7 @@ class AwesomeFragment : BaseFragment<FragmentAwesomeBinding>() {
model.primaryAction = { onCancel(this) }
} else {
model.primaryButtonText = "Shielding Funds..."
if(isCreated()) model.details.add("Submitting transaction...")
if (isCreated()) model.details.add("Submitting transaction...")
}
}
}
@ -301,7 +294,7 @@ class AwesomeFragment : BaseFragment<FragmentAwesomeBinding>() {
val details: MutableSet<String> = linkedSetOf(),
var showProgress: Boolean = false,
var primaryButtonText: String = "Shield Transparent Funds",
var primaryAction: () -> Unit = {},
var primaryAction: () -> Unit = {},
var canCancel: 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.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.
*/
class FeedbackFragment : BaseFragment<FragmentFeedbackBinding>() {
class FeedbackFragment : BaseFragment<FragmentFeedbackBinding>() {
override val screen = Report.Screen.FEEDBACK
val args: FeedbackFragmentArgs by navArgs()
@ -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() {
@ -135,7 +135,7 @@ class ProfileFragment : BaseFragment<FragmentProfileBinding>() {
viewModel.quickRescan()
Toast.makeText(ZcashWalletApp.instance, "Performing quick rescan!", Toast.LENGTH_LONG).show()
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}")
}
}
@ -145,8 +145,8 @@ class ProfileFragment : BaseFragment<FragmentProfileBinding>() {
mainActivity?.showConfirmation(
"Are you sure?",
"Wiping your data will close the app. Since your seed is preserved, " +
"this operation is probably safe but please backup your seed anyway." +
"\n\nContinue?",
"this operation is probably safe but please backup your seed anyway." +
"\n\nContinue?",
"Wipe"
) {
viewModel.wipe()

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
@ -108,7 +106,7 @@ class ProfileViewModel @Inject constructor() : ViewModel() {
fun quickScanDistance(): Int {
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
runBlocking {
foo = synchronizer.getNearestRewindHeight(latest - oneWeek)

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 {
bindPreview(cameraProviderFuture.get())
}, ContextCompat.getMainExecutor(context))
cameraProviderFuture.addListener(
Runnable {
bindPreview(cameraProviderFuture.get())
},
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 ->
onQrScanned(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
@ -127,7 +138,7 @@ class ScanFragment : BaseFragment<FragmentScanBinding>() {
val network = viewModel.networkName
binding.textScanError.text = getString(R.string.scan_invalid_address, network, qrContent)
image.close()
} else { /* continue scanning*/
} else { /* continue scanning*/
binding.textScanError.text = ""
sendViewModel.toAddress = parsed
mainActivity?.safeNavigate(R.id.action_nav_scan_to_nav_send)
@ -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

@ -13,7 +13,7 @@ class ScanViewModel @Inject constructor() : ViewModel() {
val networkName get() = synchronizer.network.networkName
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
val address = if (qrCode.startsWith("zcash:")) {
qrCode.substring(6, qrCode.indexOf("?").takeUnless { it == -1 } ?: qrCode.length)
@ -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
@ -70,13 +75,13 @@ class SendFinalFragment : BaseFragment<FragmentSendFinalBinding>() {
}
buttonMoreInfo.apply {
setOnClickListener{
setOnClickListener {
val moreInfoMsg = """${getString(R.string.more_info)} : ${model.errorDescription}"""
txtMoreInfo.run {
text = moreInfoMsg
}
if(model.errorDescription.isNotEmpty())
if (model.errorDescription.isNotEmpty())
buttonMoreInfo.text = getString(R.string.translated_button_done)
}
}
@ -115,7 +120,7 @@ class SendFinalFragment : BaseFragment<FragmentSendFinalBinding>() {
private fun PendingTransaction.toUiModel() = UiModel().also { model ->
when {
isCancelled() -> {
isCancelled() -> {
model.title = getString(R.string.send_final_result_cancelled)
model.primaryButtonText = getString(R.string.send_final_button_primary_back)
model.primaryAction = { onReturnToSend() }
@ -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() }
@ -156,7 +162,6 @@ class SendFinalFragment : BaseFragment<FragmentSendFinalBinding>() {
var showProgress: Boolean = false,
var errorMessage: String = "",
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.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,14 +64,13 @@ class SendFragment : BaseFragment<FragmentSendBinding>(),
applyViewModel(sendViewModel)
updateAddressUi(false)
// Apply behaviors
binding.buttonSend.setOnClickListener {
onSubmit().also { tapped(SEND_SUBMIT) }
}
binding.checkIncludeAddress.setOnCheckedChangeListener { _, _->
binding.checkIncludeAddress.setOnCheckedChangeListener { _, _ ->
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) {
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)
}
@ -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()))
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")
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())
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())
}
}
}
@ -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
@ -41,7 +45,7 @@ class SendMemoFragment : BaseFragment<FragmentSendMemoBinding>() {
// onBackPressNavTo(it) { tapped(SEND_MEMO_BACK) }
// }
binding.checkIncludeAddress.setOnCheckedChangeListener { _, _->
binding.checkIncludeAddress.setOnCheckedChangeListener { _, _ ->
onIncludeMemo(binding.checkIncludeAddress.isChecked)
}

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
@ -54,7 +64,7 @@ class SendViewModel @Inject constructor() : ViewModel() {
set(value) {
require(!value || (value && !fromAddress.isNullOrEmpty())) {
"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
}
@ -92,7 +102,7 @@ class SendViewModel @Inject constructor() : ViewModel() {
suspend fun validateAddress(address: String): AddressType =
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
else -> false
}
@ -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 {
@ -228,26 +237,17 @@ class SendViewModel @Inject constructor() : ViewModel() {
metrics[metricId].also { if (it == null) println("Warning no start metric for id: $metricId") }
}
return startMetric?.endTime?.let { startMetricEndTime ->
TimeMetric(second.key, second.description, mutableListOf(startMetricEndTime))
.markTime().let { endMetric ->
endMetric.toMetricIdFor(txId).also { metricId ->
metrics[metricId] = endMetric
metrics[metricId.toRelatedMetricId()] = startMetric
}
TimeMetric(second.key, second.description, mutableListOf(startMetricEndTime))
.markTime().let { endMetric ->
endMetric.toMetricIdFor(txId).also { metricId ->
metrics[metricId] = endMetric
metrics[metricId.toRelatedMetricId()] = startMetric
}
}
}
}
}
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
//
@ -131,24 +136,22 @@ class SettingsFragment : BaseFragment<FragmentSettingsBinding>() {
error.javaClass.simpleName
}
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)
Toast.makeText(ZcashWalletApp.instance, getString(R.string.settings_toast_change_server_failure), Toast.LENGTH_SHORT).show()
context?.showUpdateServerCriticalError(message)
}
//
// Utilities
//
private fun String?.toHelperTextColor(): ColorStateList {
val color = if (this == null) {
val color = if (this == null) {
R.color.text_light_dimmed
} else {
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.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

@ -40,7 +40,7 @@ class BackupFragment : BaseFragment<FragmentBackupBinding>() {
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 =
FragmentBackupBinding.inflate(inflater)
@ -76,7 +76,7 @@ class BackupFragment : BaseFragment<FragmentBackupBinding>() {
override fun onAttach(context: Context) {
super.onAttach(context)
walletSetup.checkSeed().onEach {
hasBackUp = when(it) {
hasBackUp = when (it) {
SEED_WITH_BACKUP -> true
else -> false
}
@ -134,8 +134,8 @@ class BackupFragment : BaseFragment<FragmentBackupBinding>() {
mainActivity!!.feedback.measure(SEED_PHRASE_LOADED) {
val lockBox = LockBox(ZcashWalletApp.instance)
val mnemonics = Mnemonics()
val seedPhrase = lockBox.getCharsUtf8(Const.Backup.SEED_PHRASE) ?: throw RuntimeException("Seed Phrase expected but not found in storage!!")
val result = mnemonics.toWordList(seedPhrase)
val seedPhrase = lockBox.getCharsUtf8(Const.Backup.SEED_PHRASE) ?: throw RuntimeException("Seed Phrase expected but not found in storage!!")
val result = mnemonics.toWordList(seedPhrase)
result
}
}

View File

@ -89,7 +89,7 @@ class LandingFragment : BaseFragment<FragmentLandingBinding>() {
super.onAttach(context)
walletSetup.checkSeed().onEach {
when(it) {
when (it) {
SEED_WITHOUT_BACKUP, SEED_WITH_BACKUP -> {
mainActivity?.safeNavigate(R.id.nav_backup)
}
@ -100,9 +100,12 @@ class LandingFragment : BaseFragment<FragmentLandingBinding>() {
override fun onResume() {
super.onResume()
view?.postDelayed({
mainActivity?.hideKeyboard()
}, 25L)
view?.postDelayed(
{
mainActivity?.hideKeyboard()
},
25L
)
}
private fun onSkip(count: Int) {
@ -134,10 +137,10 @@ class LandingFragment : BaseFragment<FragmentLandingBinding>() {
val birthday: Int
// new testnet dev wallet
when(ZcashWalletApp.instance.defaultNetwork) {
when (ZcashWalletApp.instance.defaultNetwork) {
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"
birthday = 991645 //663174
birthday = 991645 // 663174
}
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"

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({
val isDone = (seedWordAdapter?.itemCount ?: 0) > 24
val focusedView = if (isDone) binding.inputBirthdate else seedWordAdapter!!.editText
mainActivity!!.showKeyboard(focusedView)
focusedView.requestFocus()
}, 500L)
requireView().postDelayed(
{
val isDone = (seedWordAdapter?.itemCount ?: 0) > 24
val focusedView = if (isDone) binding.inputBirthdate else seedWordAdapter!!.editText
mainActivity!!.showKeyboard(focusedView)
focusedView.requestFocus()
},
500L
)
}
private fun reportWords(count: Int) {
@ -220,11 +218,14 @@ class RestoreFragment : BaseFragment<FragmentRestoreBinding>(), View.OnKeyListen
private fun touchScreenForUser() {
seedWordAdapter?.editText?.apply {
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)
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
)
}
}
@ -235,11 +236,10 @@ 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() {
override fun getSubtitle(): String? = null//"subtitle for $word"
override fun getSubtitle(): String? = null // "subtitle for $word"
override fun getAvatarDrawable(): Drawable? = null
override fun getId() = index
override fun getTitle() = word

View File

@ -7,10 +7,9 @@ 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 {
class SeedWordAdapter : ChipsAdapter {
constructor(existingAdapter: ChipsAdapter) : super(existingAdapter.mDataSource, existingAdapter.mEditText, existingAdapter.mOptions)
@ -22,23 +21,23 @@ class SeedWordAdapter : ChipsAdapter {
else object : RecyclerView.ViewHolder(mEditText) {}
}
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
(holder as SeedWordHolder).seedChipView.bind(mDataSource.getSelectedChip(position), position);
(holder as SeedWordHolder).seedChipView.bind(mDataSource.getSelectedChip(position), position)
} else {
val size = mDataSource.selectedChips.size
// tricky bugfix:
// 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
// editing. This is not ideal but it's better than a crash during wallet restore!
// tricky bugfix:
// 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
// editing. This is not ideal but it's better than a crash during wallet restore!
mEditText.isEnabled = true
mEditText.hint = if (size < 3) {
mEditText.isCursorVisible = true
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"
} else if(size >= 24) {
} else if (size >= 24) {
mEditText.setHintTextColor(R.color.zcashGreen.toAppColor())
mEditText.isCursorVisible = false
"done"
@ -66,10 +65,13 @@ class SeedWordAdapter : ChipsAdapter {
if (mDataSource.originalChips.firstOrNull { it.title == text } != null) {
mDataSource.addSelectedChip(DefaultCustomChip(text))
mEditText.apply {
postDelayed({
setText("")
requestFocus()
}, 50L)
postDelayed(
{
setText("")
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) =
WalletBirthdayTool.loadNearest(ZcashWalletApp.instance, network, birthdayHeight)
//
// Storage Helpers
//
@ -193,7 +192,7 @@ class WalletSetupViewModel @Inject constructor() : ViewModel() {
) {
check(!lockBox.getBoolean(Const.Backup.HAS_SEED)) {
"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)
@ -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

@ -16,13 +16,13 @@ class AddressPartNumberSpan(
) : MetricAffectingSpan() {
override fun updateMeasureState(textPaint: TextPaint) {
textPaint.baselineShift += (textPaint.ascent() / 2).toInt() // from SuperscriptSpan
textPaint.textSize = textPaint.textSize * proportion // from RelativeSizeSpan
textPaint.baselineShift += (textPaint.ascent() / 2).toInt() // from SuperscriptSpan
textPaint.textSize = textPaint.textSize * proportion // from RelativeSizeSpan
}
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.textSize = textPaint.textSize * proportion // from RelativeSizeSpan
textPaint.color = color // from ForegroundColorSpan
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.color = color // from ForegroundColorSpan
}
}

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.
*/
val INCLUDE_MEMO_PREFIXES_RECOGNIZED = arrayOf(
INCLUDE_MEMO_PREFIX_STANDARD, // standard
"reply-to", // standard w/o colon
"reply to:", // space instead of dash
"reply to", // space instead of dash w/o colon
"sent from:", // previous standard
"sent from" // previous standard w/o colon
INCLUDE_MEMO_PREFIX_STANDARD, // standard
"reply-to", // standard w/o colon
"reply to:", // space instead of dash
"reply to", // space instead of dash w/o colon
"sent from:", // previous standard
"sent from" // previous standard w/o colon
)
// TODO: move this to the SDK
@ -41,7 +41,7 @@ object MemoUtil {
INCLUDE_MEMO_PREFIXES_RECOGNIZED.mapNotNull {
val maybeMemo = memo.substringAfterLast(it)
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
//
//import android.Manifest
//import android.content.Context
//import android.content.pm.PackageManager
//import android.os.Bundle
//import android.widget.Toast
//import androidx.core.content.ContextCompat
//import androidx.fragment.app.Fragment
//import cash.z.ecc.android.ui.MainActivity
// import android.Manifest
// import android.content.Context
// import android.content.pm.PackageManager
// import android.os.Bundle
// import android.widget.Toast
// import androidx.core.content.ContextCompat
// import androidx.fragment.app.Fragment
// import cash.z.ecc.android.ui.MainActivity
//
//class PermissionFragment : Fragment() {
// class PermissionFragment : Fragment() {
//
// val activity get() = context as MainActivity
//
@ -44,4 +44,4 @@ package cash.z.ecc.android.ui.util
// ContextCompat.checkSelfPermission(context, it) == PackageManager.PERMISSION_GRANTED
// }
// }
//}
// }

View File

@ -1,16 +1,17 @@
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 {
val t get() = System.currentTimeMillis()
val t get() = System.currentTimeMillis()
var t0 = 0L
val Δt get() = t - t0
@ -23,10 +24,10 @@ 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")}
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") }
combine(flow, flow2, flow3) { i, s, t -> "$i$s$t" }.onStart {
t0 = t
}.collect {
@ -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")}"
}