From 3e6355b0c7056e7e1cb2d257c294a68bd132050a Mon Sep 17 00:00:00 2001 From: Kevin Gorham Date: Fri, 11 Sep 2020 03:16:46 -0400 Subject: [PATCH 01/12] Revamped demo app. - Add logging - Add service to mainactivity - Use new SDK derivation tool - Add prev/next buttons to block demo - Add helpful stats to block range demo - Add instructions to home screen and stop clearing after each demo - Improve transaction list display - Revamped every demo - Added HTML to displays - Added helpful extension functions - Updated dependencies --- samples/demo-app/app/build.gradle | 18 +-- .../cash/z/ecc/android/sdk/demoapp/App.kt | 3 + .../android/sdk/demoapp/BaseDemoFragment.kt | 19 +-- .../z/ecc/android/sdk/demoapp/MainActivity.kt | 38 +++++- .../demos/getaddress/GetAddressFragment.kt | 26 ++-- .../demos/getblock/GetBlockFragment.kt | 87 ++++++++----- .../getblockrange/GetBlockRangeFragment.kt | 114 ++++++++++++----- .../GetLatestHeightFragment.kt | 49 ++++---- .../getprivatekey/GetPrivateKeyFragment.kt | 24 ++-- .../sdk/demoapp/demos/home/HomeFragment.kt | 7 +- .../ListTransactionsFragment.kt | 95 +++++++++------ .../listtransactions/TransactionViewHolder.kt | 14 ++- .../demos/listutxos/ListUtxosFragment.kt | 22 +--- .../sdk/demoapp/demos/send/SendFragment.kt | 115 +++++++++++------- .../z/ecc/android/sdk/demoapp/util/Ext.kt | 37 +++++- .../main/res/layout/fragment_get_address.xml | 5 +- .../main/res/layout/fragment_get_block.xml | 67 +++++++--- .../res/layout/fragment_get_block_range.xml | 30 +++-- .../res/layout/fragment_get_private_key.xml | 2 +- .../app/src/main/res/layout/fragment_home.xml | 15 +++ .../app/src/main/res/layout/fragment_send.xml | 33 ++--- .../src/main/res/layout/item_transaction.xml | 4 +- .../app/src/main/res/values/colors.xml | 3 + .../app/src/main/res/values/strings.xml | 3 + .../z/ecc/android/sdk/demoapp/DemoConfig.kt | 11 +- samples/demo-app/build.gradle | 6 +- 26 files changed, 547 insertions(+), 300 deletions(-) diff --git a/samples/demo-app/app/build.gradle b/samples/demo-app/app/build.gradle index 42243ba6..0e6f7bd2 100644 --- a/samples/demo-app/app/build.gradle +++ b/samples/demo-app/app/build.gradle @@ -49,8 +49,8 @@ android { dependencies { // SDK - zcashmainnetImplementation 'cash.z.ecc.android:sdk-mainnet:1.1.0-beta04' - zcashtestnetImplementation 'cash.z.ecc.android:sdk-testnet:1.1.0-beta04' + zcashmainnetImplementation 'cash.z.ecc.android:sdk-mainnet:1.1.0-beta05' + zcashtestnetImplementation 'cash.z.ecc.android:sdk-testnet:1.1.0-beta05' // sample mnemonic plugin implementation 'com.github.zcash:zcash-android-wallet-plugins:1.0.1' @@ -68,20 +68,20 @@ dependencies { implementation 'com.google.guava:guava:27.0.1-android' kapt 'androidx.room:room-compiler:2.2.5' // SDK: Other - implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.8' - implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.8' + implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.9' + implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.9' // Android implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" implementation 'androidx.core:core-ktx:1.3.1' - implementation 'androidx.constraintlayout:constraintlayout:1.1.3' + implementation 'androidx.constraintlayout:constraintlayout:2.0.1' implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0' - implementation 'androidx.lifecycle:lifecycle-common-java8:2.3.0-alpha06' - implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.3.0-alpha06' // provides lifecycleScope! + implementation 'androidx.lifecycle:lifecycle-common-java8:2.3.0-alpha07' + implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.3.0-alpha07' // provides lifecycleScope! implementation 'androidx.navigation:navigation-fragment-ktx:2.3.0' implementation 'androidx.navigation:navigation-ui-ktx:2.3.0' implementation "com.google.android.material:material:1.3.0-alpha02" testImplementation 'junit:junit:4.13' - androidTestImplementation 'androidx.test.ext:junit:1.1.1' - androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' + androidTestImplementation 'androidx.test.ext:junit:1.1.2' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0' } diff --git a/samples/demo-app/app/src/main/java/cash/z/ecc/android/sdk/demoapp/App.kt b/samples/demo-app/app/src/main/java/cash/z/ecc/android/sdk/demoapp/App.kt index 8fd2b828..7bcc6e7e 100644 --- a/samples/demo-app/app/src/main/java/cash/z/ecc/android/sdk/demoapp/App.kt +++ b/samples/demo-app/app/src/main/java/cash/z/ecc/android/sdk/demoapp/App.kt @@ -1,6 +1,8 @@ package cash.z.ecc.android.sdk.demoapp import android.app.Application +import cash.z.ecc.android.sdk.ext.TroubleshootingTwig +import cash.z.ecc.android.sdk.ext.Twig class App : Application() { @@ -9,6 +11,7 @@ class App : Application() { override fun onCreate() { instance = this super.onCreate() + Twig.plant(TroubleshootingTwig()) } companion object { diff --git a/samples/demo-app/app/src/main/java/cash/z/ecc/android/sdk/demoapp/BaseDemoFragment.kt b/samples/demo-app/app/src/main/java/cash/z/ecc/android/sdk/demoapp/BaseDemoFragment.kt index 35d1bd13..fcce97bf 100644 --- a/samples/demo-app/app/src/main/java/cash/z/ecc/android/sdk/demoapp/BaseDemoFragment.kt +++ b/samples/demo-app/app/src/main/java/cash/z/ecc/android/sdk/demoapp/BaseDemoFragment.kt @@ -11,21 +11,24 @@ import android.widget.Toast import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels import androidx.viewbinding.ViewBinding +import cash.z.ecc.android.sdk.demoapp.util.mainActivity import cash.z.ecc.android.sdk.ext.TroubleshootingTwig import cash.z.ecc.android.sdk.ext.Twig import com.google.android.material.snackbar.Snackbar abstract class BaseDemoFragment : Fragment() { + /** + * Since the lightwalletservice is not a component that apps typically use, directly, we provide + * this from one place. Everything that can be done with the service can/should be done with the + * synchronizer because it wraps the service. + */ + val lightwalletService get() = mainActivity()?.lightwalletService + // contains view information provided by the user val sharedViewModel: SharedViewModel by activityViewModels() lateinit var binding: T - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - Twig.plant(TroubleshootingTwig()) - } - + override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, @@ -74,11 +77,11 @@ abstract class BaseDemoFragment : Fragment() { /** * Convenience function to the given text to the clipboard. */ - open fun copyToClipboard(text: String) { + open fun copyToClipboard(text: String, description: String = "Copied to clipboard!") { (activity?.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager)?.let { cm -> cm.setPrimaryClip(ClipData.newPlainText("DemoAppClip", text)) } - toast("Copied to clipboard!") + toast(description) } /** diff --git a/samples/demo-app/app/src/main/java/cash/z/ecc/android/sdk/demoapp/MainActivity.kt b/samples/demo-app/app/src/main/java/cash/z/ecc/android/sdk/demoapp/MainActivity.kt index e5cfd61d..bf370ae1 100644 --- a/samples/demo-app/app/src/main/java/cash/z/ecc/android/sdk/demoapp/MainActivity.kt +++ b/samples/demo-app/app/src/main/java/cash/z/ecc/android/sdk/demoapp/MainActivity.kt @@ -17,6 +17,8 @@ import androidx.navigation.ui.navigateUp import androidx.navigation.ui.setupActionBarWithNavController import androidx.navigation.ui.setupWithNavController import androidx.viewbinding.ViewBinding +import cash.z.ecc.android.sdk.service.LightWalletGrpcService +import cash.z.ecc.android.sdk.service.LightWalletService import com.google.android.material.floatingactionbutton.FloatingActionButton import com.google.android.material.navigation.NavigationView @@ -27,6 +29,15 @@ class MainActivity : AppCompatActivity(), ClipboardManager.OnPrimaryClipChangedL private var clipboardListener: ((String?) -> Unit)? = null var fabListener: BaseDemoFragment? = null + /** + * The service to use for all demos that interact directly with the service. Since gRPC channels + * are expensive to recreate, we set this up once per demo. A real app would hardly ever use + * this object because it would utilize the synchronizer, instead, which exposes APIs that + * automatically sync with the server. + */ + var lightwalletService: LightWalletService? = null + private set + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) clipboard = getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager @@ -54,12 +65,15 @@ class MainActivity : AppCompatActivity(), ClipboardManager.OnPrimaryClipChangedL setupActionBarWithNavController(navController, appBarConfiguration) navView.setupWithNavController(navController) drawerLayout.addDrawerListener(this) + + initService() } - private fun onFabClicked(view: View) { - fabListener?.onActionButtonClicked() + override fun onDestroy() { + super.onDestroy() + lightwalletService?.shutdown() } - + override fun onCreateOptionsMenu(menu: Menu): Boolean { // Inflate the menu; this adds items to the action bar if it is present. menuInflater.inflate(R.menu.main, menu) @@ -82,6 +96,24 @@ class MainActivity : AppCompatActivity(), ClipboardManager.OnPrimaryClipChangedL } + // + // Private functions + // + + private fun initService() { + if (lightwalletService != null) { + lightwalletService?.shutdown() + } + with(App.instance.defaultConfig) { + lightwalletService = LightWalletGrpcService(App.instance, host, port) + } + } + + private fun onFabClicked(view: View) { + fabListener?.onActionButtonClicked() + } + + // // Helpers // diff --git a/samples/demo-app/app/src/main/java/cash/z/ecc/android/sdk/demoapp/demos/getaddress/GetAddressFragment.kt b/samples/demo-app/app/src/main/java/cash/z/ecc/android/sdk/demoapp/demos/getaddress/GetAddressFragment.kt index 6482944b..d082a103 100644 --- a/samples/demo-app/app/src/main/java/cash/z/ecc/android/sdk/demoapp/demos/getaddress/GetAddressFragment.kt +++ b/samples/demo-app/app/src/main/java/cash/z/ecc/android/sdk/demoapp/demos/getaddress/GetAddressFragment.kt @@ -4,10 +4,9 @@ import android.os.Bundle import android.view.LayoutInflater import cash.z.ecc.android.bip39.Mnemonics import cash.z.ecc.android.bip39.toSeed -import cash.z.ecc.android.sdk.Initializer -import cash.z.ecc.android.sdk.demoapp.App import cash.z.ecc.android.sdk.demoapp.BaseDemoFragment import cash.z.ecc.android.sdk.demoapp.databinding.FragmentGetAddressBinding +import cash.z.ecc.android.sdk.tool.DerivationTool /** * Displays the address associated with the seed defined by the default config. To modify the seed @@ -15,7 +14,6 @@ import cash.z.ecc.android.sdk.demoapp.databinding.FragmentGetAddressBinding */ class GetAddressFragment : BaseDemoFragment() { - private lateinit var initializer: Initializer private lateinit var viewingKey: String private lateinit var seed: ByteArray @@ -23,7 +21,7 @@ class GetAddressFragment : BaseDemoFragment() { * Initialize the required values that would normally live outside the demo but are repeated * here for completeness so that each demo file can serve as a standalone example. */ - fun setup() { + private fun setup() { // defaults to the value of `DemoConfig.seedWords` but can also be set by the user var seedPhrase = sharedViewModel.seedPhrase.value @@ -31,17 +29,16 @@ class GetAddressFragment : BaseDemoFragment() { // have the seed stored seed = Mnemonics.MnemonicCode(seedPhrase).toSeed() - // the initializer loads rust libraries and helps with configuration - initializer = Initializer(App.instance) - - // demonstrate deriving viewing keys for five accounts but only take the first one - viewingKey = initializer.deriveViewingKeys(seed).first() + // the derivation tool can be used for generating keys and addresses + viewingKey = DerivationTool.deriveViewingKeys(seed).first() } - fun displayAddress() { + private fun displayAddress() { // alternatively, `deriveAddress` can take the seed as a parameter instead - val address = initializer.deriveAddress(viewingKey) - binding.textInfo.text = address + // although, a full fledged app would just get the address from the synchronizer + val zaddress = DerivationTool.deriveShieldedAddress(viewingKey) + val taddress = DerivationTool.deriveTransparentAddress(seed) + binding.textInfo.text = "z-addr:\n$zaddress\n\n\nt-addr:\n$taddress" } // TODO: show an example with the synchronizer @@ -66,7 +63,10 @@ class GetAddressFragment : BaseDemoFragment() { // override fun onActionButtonClicked() { - copyToClipboard(initializer.deriveAddress(viewingKey)) + copyToClipboard( + DerivationTool.deriveTransparentAddress(seed), + "Shielded address copied to clipboard!" + ) } override fun inflateBinding(layoutInflater: LayoutInflater): FragmentGetAddressBinding = diff --git a/samples/demo-app/app/src/main/java/cash/z/ecc/android/sdk/demoapp/demos/getblock/GetBlockFragment.kt b/samples/demo-app/app/src/main/java/cash/z/ecc/android/sdk/demoapp/demos/getblock/GetBlockFragment.kt index 54985a24..b7ef4951 100644 --- a/samples/demo-app/app/src/main/java/cash/z/ecc/android/sdk/demoapp/demos/getblock/GetBlockFragment.kt +++ b/samples/demo-app/app/src/main/java/cash/z/ecc/android/sdk/demoapp/demos/getblock/GetBlockFragment.kt @@ -1,12 +1,16 @@ package cash.z.ecc.android.sdk.demoapp.demos.getblock +import android.os.Bundle +import android.text.Html import android.view.LayoutInflater import android.view.View -import cash.z.ecc.android.sdk.demoapp.App import cash.z.ecc.android.sdk.demoapp.BaseDemoFragment import cash.z.ecc.android.sdk.demoapp.databinding.FragmentGetBlockBinding -import cash.z.ecc.android.sdk.service.LightWalletGrpcService -import cash.z.ecc.android.sdk.service.LightWalletService +import cash.z.ecc.android.sdk.demoapp.util.mainActivity +import cash.z.ecc.android.sdk.demoapp.util.toHtml +import cash.z.ecc.android.sdk.demoapp.util.toRelativeTime +import cash.z.ecc.android.sdk.demoapp.util.withCommas +import cash.z.ecc.android.sdk.ext.toHex /** * Retrieves a compact block from the lightwalletd service and displays basic information about it. @@ -14,39 +18,60 @@ import cash.z.ecc.android.sdk.service.LightWalletService * the response. */ class GetBlockFragment : BaseDemoFragment() { - private val host = App.instance.defaultConfig.host - private val port = App.instance.defaultConfig.port - - private lateinit var lightwalletService: LightWalletService - - override fun inflateBinding(layoutInflater: LayoutInflater): FragmentGetBlockBinding = - FragmentGetBlockBinding.inflate(layoutInflater) - - fun resetInBackground() { - lightwalletService = LightWalletGrpcService(App.instance, host, port) - } - - fun onResetComplete() { - binding.buttonApply.setOnClickListener(::onApply) - onApply(binding.textBlockHeight) - } - - private fun onApply(_unused: View) { - setBlockHeight(binding.textBlockHeight.text.toString().toInt()) - } private fun setBlockHeight(blockHeight: Int) { val blocks = - lightwalletService.getBlockRange(blockHeight..blockHeight) - val block = blocks.firstOrNull() - binding.textInfo.text = """ - block height: ${block?.height} - block vtxCount: ${block?.vtxCount} - block time: ${block?.time} + lightwalletService?.getBlockRange(blockHeight..blockHeight) + val block = blocks?.firstOrNull() + binding.textInfo.visibility = View.VISIBLE + binding.textInfo.text = Html.fromHtml( + """ + block height: ${block?.height.withCommas()} +
block time: ${block?.time.toRelativeTime()} +
number of shielded TXs: ${block?.vtxCount} +
hash: ${block?.hash?.toByteArray()?.toHex()} +
prevHash: ${block?.prevHash?.toByteArray()?.toHex()} + ${block?.vtxList.toHtml()} """.trimIndent() + ) } - fun onClear() { - lightwalletService.shutdown() + private fun onApply(_unused: View? = null) { + try { + setBlockHeight(binding.textBlockHeight.text.toString().toInt()) + } catch (t: Throwable) { + toast("Error: $t") + } + mainActivity()?.hideKeyboard() } + + private fun loadNext(offset: Int) { + val nextBlockHeight = (binding.textBlockHeight.text.toString().toIntOrNull() ?: -1) + offset + binding.textBlockHeight.setText(nextBlockHeight.toString()) + onApply() + } + + + // + // Android Lifecycle overrides + // + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + binding.buttonApply.setOnClickListener(::onApply) + binding.buttonPrevious.setOnClickListener { + loadNext(-1) + } + binding.buttonNext.setOnClickListener { + loadNext(1) + } + } + + + // + // Base Fragment overrides + // + + override fun inflateBinding(layoutInflater: LayoutInflater): FragmentGetBlockBinding = + FragmentGetBlockBinding.inflate(layoutInflater) } diff --git a/samples/demo-app/app/src/main/java/cash/z/ecc/android/sdk/demoapp/demos/getblockrange/GetBlockRangeFragment.kt b/samples/demo-app/app/src/main/java/cash/z/ecc/android/sdk/demoapp/demos/getblockrange/GetBlockRangeFragment.kt index f30a9d8f..28855363 100644 --- a/samples/demo-app/app/src/main/java/cash/z/ecc/android/sdk/demoapp/demos/getblockrange/GetBlockRangeFragment.kt +++ b/samples/demo-app/app/src/main/java/cash/z/ecc/android/sdk/demoapp/demos/getblockrange/GetBlockRangeFragment.kt @@ -1,65 +1,113 @@ package cash.z.ecc.android.sdk.demoapp.demos.getblockrange +import android.os.Bundle +import android.text.Html import android.view.LayoutInflater import android.view.View -import cash.z.ecc.android.sdk.demoapp.App import cash.z.ecc.android.sdk.demoapp.BaseDemoFragment +import cash.z.ecc.android.sdk.demoapp.R import cash.z.ecc.android.sdk.demoapp.databinding.FragmentGetBlockRangeBinding -import cash.z.ecc.android.sdk.service.LightWalletGrpcService -import cash.z.ecc.android.sdk.service.LightWalletService +import cash.z.ecc.android.sdk.demoapp.util.mainActivity +import cash.z.ecc.android.sdk.demoapp.util.toRelativeTime +import cash.z.ecc.android.sdk.demoapp.util.withCommas /** * Retrieves a range of compact block from the lightwalletd service and displays basic information - * about them. This demonstrates the basic ability to connect to the server, request a range of + * about them. This demonstrates the basic ability to connect to the server, request a range of * compact block and parse the response. This could be augmented to display metadata about certain * block ranges for instance, to find the block with the most shielded transactions in a range. */ class GetBlockRangeFragment : BaseDemoFragment() { - private val host = App.instance.defaultConfig.host - private val port = App.instance.defaultConfig.port + private fun setBlockRange(blockRange: IntRange) { + val start = System.currentTimeMillis() + val blocks = + lightwalletService?.getBlockRange(blockRange) + val fetchDelta = System.currentTimeMillis() - start - private lateinit var lightwalletService: LightWalletService - - override fun inflateBinding(layoutInflater: LayoutInflater): FragmentGetBlockRangeBinding = - FragmentGetBlockRangeBinding.inflate(layoutInflater) + // Note: This is a demo so we won't worry about iterating efficiently over these blocks - fun resetInBackground() { - lightwalletService = LightWalletGrpcService(App.instance, host, port) - } + binding.textInfo.text = Html.fromHtml(blocks?.run { + val count = size + val emptyCount = count { it.vtxCount == 0 } + val maxTxs = maxByOrNull { it.vtxCount } + val maxIns = maxByOrNull { block -> + block.vtxList.maxOfOrNull { it.spendsCount } ?: -1 + } + val maxInTx = maxIns?.vtxList?.maxByOrNull { it.spendsCount } + val maxOuts = maxByOrNull { block -> + block.vtxList.maxOfOrNull { it.outputsCount } ?: -1 + } + val maxOutTx = maxOuts?.vtxList?.maxByOrNull { it.outputsCount } + val txCount = sumBy { it.vtxCount } + val outCount = sumBy { block -> block.vtxList.sumBy { it.outputsCount } } + val inCount = sumBy { block -> block.vtxList.sumBy { it.spendsCount } } - fun onResetComplete() { - binding.buttonApply.setOnClickListener(::onApply) - onApply(binding.textInfo) - } - - fun onClear() { - lightwalletService.shutdown() + val processTime = System.currentTimeMillis() - start - fetchDelta + """ + total blocks: ${count.withCommas()} +
fetch time: ${if(fetchDelta > 1000) "%.2f sec".format(fetchDelta/1000.0) else "%d ms".format(fetchDelta)} +
process time: ${if(processTime > 1000) "%.2f sec".format(processTime/1000.0) else "%d ms".format(processTime)} +
block time range: ${first().time.toRelativeTime()}
   to ${last().time.toRelativeTime()} +
total empty blocks: ${emptyCount.withCommas()} +
total TXs: ${txCount.withCommas()} +
total outputs: ${outCount.withCommas()} +
total inputs: ${inCount.withCommas()} +
avg TXs/block: ${"%.1f".format(txCount/count.toDouble())} +
avg TXs (excluding empty blocks): ${"%.1f".format(txCount.toDouble()/(count - emptyCount))} +
avg OUTs [per block / per TX]: ${"%.1f / %.1f".format(outCount.toDouble()/(count - emptyCount), outCount.toDouble()/txCount)} +
avg INs [per block / per TX]: ${"%.1f / %.1f".format(inCount.toDouble()/(count - emptyCount), inCount.toDouble()/txCount)} +
most shielded TXs: ${if(maxTxs==null) "none" else "${maxTxs.vtxCount} in block ${maxTxs.height.withCommas()}"} +
most shielded INs: ${if(maxInTx==null) "none" else "${maxInTx.spendsCount} in block ${maxIns?.height.withCommas()} at tx index ${maxInTx.index}"} +
most shielded OUTs: ${if(maxOutTx==null) "none" else "${maxOutTx?.outputsCount} in block ${maxOuts?.height.withCommas()} at tx index ${maxOutTx?.index}"} + """.trimIndent() + } ?: "No blocks found in that range.") } private fun onApply(_unused: View) { val start = binding.textStartHeight.text.toString().toInt() val end = binding.textEndHeight.text.toString().toInt() if (start <= end) { - setBlockRange(start..end) + try { + with(binding.buttonApply) { + isEnabled = false + setText(R.string.loading) + binding.textInfo.setText(R.string.loading) + post { + setBlockRange(start..end) + isEnabled = true + setText(R.string.apply) + } + } + } catch (t: Throwable) { + setError(t.toString()) + } } else { setError("Invalid range") } - } - - // TODO: iterate on this demo to show all the blocks in a recyclerview showing block heights and vtx count - private fun setBlockRange(blockRange: IntRange) { - val blocks = - lightwalletService.getBlockRange(blockRange) - val block = blocks.firstOrNull() - binding.textInfo.text = """ - block height: ${block?.height} - block vtxCount: ${block?.vtxCount} - block time: ${block?.time} - """.trimIndent() + mainActivity()?.hideKeyboard() } private fun setError(message: String) { binding.textInfo.text = "Error: $message" } + + + // + // Android Lifecycle overrides + // + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + binding.buttonApply.setOnClickListener(::onApply) + } + + + // + // Base Fragment overrides + // + + override fun inflateBinding(layoutInflater: LayoutInflater): FragmentGetBlockRangeBinding = + FragmentGetBlockRangeBinding.inflate(layoutInflater) + } diff --git a/samples/demo-app/app/src/main/java/cash/z/ecc/android/sdk/demoapp/demos/getlatestheight/GetLatestHeightFragment.kt b/samples/demo-app/app/src/main/java/cash/z/ecc/android/sdk/demoapp/demos/getlatestheight/GetLatestHeightFragment.kt index 744fad01..9b6323aa 100644 --- a/samples/demo-app/app/src/main/java/cash/z/ecc/android/sdk/demoapp/demos/getlatestheight/GetLatestHeightFragment.kt +++ b/samples/demo-app/app/src/main/java/cash/z/ecc/android/sdk/demoapp/demos/getlatestheight/GetLatestHeightFragment.kt @@ -1,41 +1,38 @@ package cash.z.ecc.android.sdk.demoapp.demos.getlatestheight import android.view.LayoutInflater -import android.view.View -import cash.z.ecc.android.sdk.demoapp.App import cash.z.ecc.android.sdk.demoapp.BaseDemoFragment import cash.z.ecc.android.sdk.demoapp.databinding.FragmentGetLatestHeightBinding -import cash.z.ecc.android.sdk.service.LightWalletGrpcService -import cash.z.ecc.android.sdk.service.LightWalletService /** * Retrieves the latest block height from the lightwalletd server. This is the simplest test for - * connectivity with the server. Modify the `host` and the `port` to check the SDK's ability to - * communicate with a given lightwalletd instance. + * connectivity with the server. Modify the `host` and the `port` inside of + * `App.instance.defaultConfig` to check the SDK's ability to communicate with a given lightwalletd + * instance. */ class GetLatestHeightFragment : BaseDemoFragment() { - private val host = App.instance.defaultConfig.host - private val port = App.instance.defaultConfig.port - private lateinit var lightwalletService: LightWalletService + private fun displayLatestHeight() { + // note: this is a blocking call, a real app wouldn't do this on the main thread + // instead, a production app would leverage the synchronizer like in the other demos + binding.textInfo.text = lightwalletService?.getLatestBlockHeight().toString() + } + + + // + // Android Lifecycle overrides + // + + override fun onResume() { + super.onResume() + displayLatestHeight() + } + + + // + // Base Fragment overrides + // override fun inflateBinding(layoutInflater: LayoutInflater): FragmentGetLatestHeightBinding = FragmentGetLatestHeightBinding.inflate(layoutInflater) - - fun resetInBackground() { - lightwalletService = LightWalletGrpcService(App.instance, host, port) - } - - fun onResetComplete() { - binding.textInfo.text = lightwalletService.getLatestBlockHeight().toString() - } - - fun onClear() { - lightwalletService.shutdown() - } - - override fun onActionButtonClicked() { - toast("Refreshed!") - onResetComplete() - } } diff --git a/samples/demo-app/app/src/main/java/cash/z/ecc/android/sdk/demoapp/demos/getprivatekey/GetPrivateKeyFragment.kt b/samples/demo-app/app/src/main/java/cash/z/ecc/android/sdk/demoapp/demos/getprivatekey/GetPrivateKeyFragment.kt index 461e6539..1965bf59 100644 --- a/samples/demo-app/app/src/main/java/cash/z/ecc/android/sdk/demoapp/demos/getprivatekey/GetPrivateKeyFragment.kt +++ b/samples/demo-app/app/src/main/java/cash/z/ecc/android/sdk/demoapp/demos/getprivatekey/GetPrivateKeyFragment.kt @@ -4,10 +4,9 @@ import android.os.Bundle import android.view.LayoutInflater import cash.z.ecc.android.bip39.Mnemonics import cash.z.ecc.android.bip39.toSeed -import cash.z.ecc.android.sdk.Initializer -import cash.z.ecc.android.sdk.demoapp.App import cash.z.ecc.android.sdk.demoapp.BaseDemoFragment import cash.z.ecc.android.sdk.demoapp.databinding.FragmentGetPrivateKeyBinding +import cash.z.ecc.android.sdk.tool.DerivationTool /** * Displays the viewing key and spending key associated with the seed used during the demo. The @@ -16,7 +15,6 @@ import cash.z.ecc.android.sdk.demoapp.databinding.FragmentGetPrivateKeyBinding */ class GetPrivateKeyFragment : BaseDemoFragment() { - private lateinit var initializer: Initializer private lateinit var seedPhrase: String private lateinit var seed: ByteArray @@ -24,10 +22,7 @@ class GetPrivateKeyFragment : BaseDemoFragment() { * Initialize the required values that would normally live outside the demo but are repeated * here for completeness so that each demo file can serve as a standalone example. */ - fun setup() { - // the initializer loads rust libraries and helps with configuration - initializer = Initializer(App.instance) - + private fun setup() { // defaults to the value of `DemoConfig.seedWords` but can also be set by the user seedPhrase = sharedViewModel.seedPhrase.value @@ -36,11 +31,13 @@ class GetPrivateKeyFragment : BaseDemoFragment() { seed = Mnemonics.MnemonicCode(seedPhrase).toSeed() } - fun displayKeys() { + private fun displayKeys() { // derive the keys from the seed: // demonstrate deriving spending keys for five accounts but only take the first one - val spendingKey = initializer.deriveSpendingKeys(seed, 5).first() - val viewingKey = initializer.deriveViewingKey(spendingKey) + val spendingKey = DerivationTool.deriveSpendingKeys(seed, 5).first() + + // derive the key that allows you to view but not spend transactions + val viewingKey = DerivationTool.deriveViewingKey(spendingKey) // display the keys in the UI binding.textInfo.setText("Spending Key:\n$spendingKey\n\nViewing Key:\n$viewingKey") @@ -66,6 +63,13 @@ class GetPrivateKeyFragment : BaseDemoFragment() { // Base Fragment overrides // + override fun onActionButtonClicked() { + copyToClipboard( + DerivationTool.deriveViewingKeys(seed, 1).first(), + "ViewingKey copied to clipboard!" + ) + } + override fun inflateBinding(layoutInflater: LayoutInflater): FragmentGetPrivateKeyBinding = FragmentGetPrivateKeyBinding.inflate(layoutInflater) diff --git a/samples/demo-app/app/src/main/java/cash/z/ecc/android/sdk/demoapp/demos/home/HomeFragment.kt b/samples/demo-app/app/src/main/java/cash/z/ecc/android/sdk/demoapp/demos/home/HomeFragment.kt index a4ad6948..11d35e6a 100644 --- a/samples/demo-app/app/src/main/java/cash/z/ecc/android/sdk/demoapp/demos/home/HomeFragment.kt +++ b/samples/demo-app/app/src/main/java/cash/z/ecc/android/sdk/demoapp/demos/home/HomeFragment.kt @@ -41,12 +41,6 @@ class HomeFragment : BaseDemoFragment() { override fun onResume() { super.onResume() - twig( - "CLEARING DATA: Visiting the home screen clears the default databases, for sanity" + - " sake, because each demo is intended to be self-contained." - ) -// App.instance.getDatabasePath("unusued.db").parentFile?.listFiles()?.forEach { it.delete() } - mainActivity()?.setClipboardListener(::updatePasteButton) lifecycleScope.launch { @@ -101,6 +95,7 @@ class HomeFragment : BaseDemoFragment() { private fun setEditShown(isShown: Boolean) { with(binding) { textSeedPhrase.visibility = if (isShown) View.GONE else View.VISIBLE + textInstructions.visibility = if (isShown) View.GONE else View.VISIBLE groupEdit.visibility = if (isShown) View.VISIBLE else View.GONE } } diff --git a/samples/demo-app/app/src/main/java/cash/z/ecc/android/sdk/demoapp/demos/listtransactions/ListTransactionsFragment.kt b/samples/demo-app/app/src/main/java/cash/z/ecc/android/sdk/demoapp/demos/listtransactions/ListTransactionsFragment.kt index 451f5752..8f1a55af 100644 --- a/samples/demo-app/app/src/main/java/cash/z/ecc/android/sdk/demoapp/demos/listtransactions/ListTransactionsFragment.kt +++ b/samples/demo-app/app/src/main/java/cash/z/ecc/android/sdk/demoapp/demos/listtransactions/ListTransactionsFragment.kt @@ -1,22 +1,22 @@ package cash.z.ecc.android.sdk.demoapp.demos.listtransactions +import android.os.Bundle import android.view.LayoutInflater import android.view.View import androidx.lifecycle.lifecycleScope import androidx.paging.PagedList import androidx.recyclerview.widget.LinearLayoutManager -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.VkInitializer import cash.z.ecc.android.sdk.block.CompactBlockProcessor +import cash.z.ecc.android.sdk.db.entity.ConfirmedTransaction import cash.z.ecc.android.sdk.demoapp.App import cash.z.ecc.android.sdk.demoapp.BaseDemoFragment import cash.z.ecc.android.sdk.demoapp.databinding.FragmentListTransactionsBinding -import cash.z.ecc.android.sdk.db.entity.ConfirmedTransaction import cash.z.ecc.android.sdk.ext.collectWith import cash.z.ecc.android.sdk.ext.twig -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.launch +import cash.z.ecc.android.sdk.tool.DerivationTool /** * List all transactions related to the given seed, since the given birthday. This begins by @@ -26,60 +26,46 @@ import kotlinx.coroutines.launch * database in a paged format that works natively with RecyclerViews. */ class ListTransactionsFragment : BaseDemoFragment() { - private val config = App.instance.defaultConfig - private val initializer = Initializer(App.instance, host = config.host, port = config.port) - private val birthday = config.loadBirthday() + private lateinit var initializer: SdkSynchronizer.SdkInitializer private lateinit var synchronizer: Synchronizer private lateinit var adapter: TransactionAdapter private lateinit var address: String private var status: Synchronizer.Status? = null - private val isSynced get() = status == Synchronizer.Status.SYNCED - override fun inflateBinding(layoutInflater: LayoutInflater): FragmentListTransactionsBinding = - FragmentListTransactionsBinding.inflate(layoutInflater) - fun resetInBackground() { - initializer.new(config.seed, birthday) + /** + * Initialize the required values that would normally live outside the demo but are repeated + * here for completeness so that each demo file can serve as a standalone example. + */ + private fun setup() { + App.instance.defaultConfig.let { config -> + initializer = VkInitializer(App.instance) { import(config.seed, config.birthdayHeight) } + address = DerivationTool.deriveShieldedAddress(config.seed) + } synchronizer = Synchronizer(initializer) } - fun onResetComplete() { - initTransactionUI() - startSynchronizer() - monitorStatus() - } - - fun onClear() { - synchronizer.stop() - initializer.clear() - } - private fun initTransactionUI() { binding.recyclerTransactions.layoutManager = LinearLayoutManager(activity, LinearLayoutManager.VERTICAL, false) adapter = TransactionAdapter() - lifecycleScope.launch { - address = synchronizer.getAddress() - synchronizer.receivedTransactions.onEach { - onTransactionsUpdated(it) - }.launchIn(this) - } binding.recyclerTransactions.adapter = adapter } - private fun startSynchronizer() { - lifecycleScope.apply { - synchronizer.start(this) - } - } - - private fun monitorStatus() { + private fun monitorChanges() { + // the lifecycleScope is used to stop everything when the fragment dies synchronizer.status.collectWith(lifecycleScope, ::onStatus) synchronizer.processorInfo.collectWith(lifecycleScope, ::onProcessorInfoUpdated) synchronizer.progress.collectWith(lifecycleScope, ::onProgress) + synchronizer.clearedTransactions.collectWith(lifecycleScope, ::onTransactionsUpdated) } + + // + // Change listeners + // + private fun onProcessorInfoUpdated(info: CompactBlockProcessor.ProcessorInfo) { if (info.isScanning) binding.textInfo.text = "Scanning blocks...${info.scanProgress}%" } @@ -108,8 +94,8 @@ class ListTransactionsFragment : BaseDemoFragment(itemView: View) : Recycler private val amountText = itemView.findViewById(R.id.text_transaction_amount) private val infoText = itemView.findViewById(R.id.text_transaction_info) private val timeText = itemView.findViewById(R.id.text_transaction_timestamp) + private val icon = itemView.findViewById(R.id.image_transaction_type) private val formatter = SimpleDateFormat("M/d h:mma", Locale.getDefault()) fun bindTo(transaction: T?) { + val isInbound = transaction?.toAddress.isNullOrEmpty() amountText.text = transaction?.value.convertZatoshiToZecString() timeText.text = if (transaction == null || transaction?.blockTimeInSeconds == 0L) "Pending" else formatter.format(transaction.blockTimeInSeconds * 1000L) infoText.text = getMemoString(transaction) + + icon.rotation = if (isInbound) 0f else 180f + icon.rotation = if (isInbound) 0f else 180f + icon.setColorFilter(ContextCompat.getColor(App.instance, if (isInbound) R.color.tx_inbound else R.color.tx_outbound)) } private fun getMemoString(transaction: T?): String { diff --git a/samples/demo-app/app/src/main/java/cash/z/ecc/android/sdk/demoapp/demos/listutxos/ListUtxosFragment.kt b/samples/demo-app/app/src/main/java/cash/z/ecc/android/sdk/demoapp/demos/listutxos/ListUtxosFragment.kt index e862dbf9..6b20f1ad 100644 --- a/samples/demo-app/app/src/main/java/cash/z/ecc/android/sdk/demoapp/demos/listutxos/ListUtxosFragment.kt +++ b/samples/demo-app/app/src/main/java/cash/z/ecc/android/sdk/demoapp/demos/listutxos/ListUtxosFragment.kt @@ -24,11 +24,11 @@ import cash.z.ecc.android.sdk.ext.twig import cash.z.ecc.android.sdk.service.LightWalletGrpcService import cash.z.ecc.android.sdk.service.LightWalletService import cash.z.ecc.android.sdk.rpc.LocalRpcTypes +import cash.z.ecc.android.sdk.tool.DerivationTool import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay import kotlinx.coroutines.launch import kotlinx.coroutines.withContext - /** * List all transactions related to the given seed, since the given birthday. This begins by * downloading any missing blocks and then validating and scanning their contents. Once scan is @@ -66,8 +66,6 @@ class ListUtxosFragment : BaseDemoFragment() { initTransactionUi() } - private lateinit var lightwalletService: LightWalletService - fun downloadTransactions() { binding.textStatus.text = "loading..." @@ -76,14 +74,13 @@ class ListUtxosFragment : BaseDemoFragment() { val addressToUse = binding.inputAddress.text.toString() val startToUse = binding.inputRangeStart.text.toString().toIntOrNull() ?: ZcashSdk.SAPLING_ACTIVATION_HEIGHT val endToUse = binding.inputRangeEnd.text.toString().toIntOrNull() ?: latestBlockHeight - lightwalletService = LightWalletGrpcService(App.instance, config.host, config.port) var allStart = now twig("loading transactions in range $startToUse..$endToUse") - val txids = lightwalletService.getTAddressTransactions(addressToUse, startToUse..endToUse) + val txids = lightwalletService?.getTAddressTransactions(addressToUse, startToUse..endToUse) var delta = now - allStart - updateStatus("found ${txids.size} transactions in ${delta}ms.", false) + updateStatus("found ${txids?.size} transactions in ${delta}ms.", false) - txids.map { + txids?.map { it.data.apply { try { initializer.rustBackend.decryptAndStoreTransaction(toByteArray()) @@ -91,7 +88,7 @@ class ListUtxosFragment : BaseDemoFragment() { twig("failed to decrypt and store transaction due to: $t") } } - }.let { txData -> + }?.let { txData -> val parseStart = now val tList = LocalRpcTypes.TransactionDataList.newBuilder().addAllData(txData).build() val parsedTransactions = initializer.rustBackend.parseTransactionDataList(tList) @@ -136,7 +133,7 @@ class ListUtxosFragment : BaseDemoFragment() { super.onResume() resetInBackground() val seed = Mnemonics.MnemonicCode(sharedViewModel.seedPhrase.value).toSeed() - binding.inputAddress.setText(initializer.rustBackend.deriveTAddress(seed)) + binding.inputAddress.setText(DerivationTool.deriveTransparentAddress(seed)) } @@ -226,13 +223,6 @@ class ListUtxosFragment : BaseDemoFragment() { private fun onTransactionsUpdated(transactions: PagedList) { twig("got a new paged list of transactions of size ${transactions.size}") adapter.submitList(transactions) - - // show message when there are no transactions -// if (isSynced) { - if (transactions.isEmpty()) { - Toast.makeText(activity, "No transactions found. Try another address or send funds to this one.", Toast.LENGTH_LONG).show() - } -// } } override fun onActionButtonClicked() { diff --git a/samples/demo-app/app/src/main/java/cash/z/ecc/android/sdk/demoapp/demos/send/SendFragment.kt b/samples/demo-app/app/src/main/java/cash/z/ecc/android/sdk/demoapp/demos/send/SendFragment.kt index 99608d78..fb15aa19 100644 --- a/samples/demo-app/app/src/main/java/cash/z/ecc/android/sdk/demoapp/demos/send/SendFragment.kt +++ b/samples/demo-app/app/src/main/java/cash/z/ecc/android/sdk/demoapp/demos/send/SendFragment.kt @@ -1,19 +1,21 @@ package cash.z.ecc.android.sdk.demoapp.demos.send +import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.widget.TextView import androidx.lifecycle.lifecycleScope -import cash.z.ecc.android.sdk.Initializer import cash.z.ecc.android.sdk.Synchronizer +import cash.z.ecc.android.sdk.VkInitializer import cash.z.ecc.android.sdk.block.CompactBlockProcessor +import cash.z.ecc.android.sdk.db.entity.* import cash.z.ecc.android.sdk.demoapp.App import cash.z.ecc.android.sdk.demoapp.BaseDemoFragment -import cash.z.ecc.android.sdk.demoapp.R import cash.z.ecc.android.sdk.demoapp.databinding.FragmentSendBinding import cash.z.ecc.android.sdk.demoapp.util.SampleStorageBridge -import cash.z.ecc.android.sdk.db.entity.* +import cash.z.ecc.android.sdk.demoapp.util.mainActivity import cash.z.ecc.android.sdk.ext.* +import cash.z.ecc.android.sdk.tool.DerivationTool /** * Demonstrates sending funds to an address. This is the most complex example that puts all of the @@ -24,16 +26,30 @@ import cash.z.ecc.android.sdk.ext.* * Any time the state of that transaction changes, a new instance will be emitted. */ class SendFragment : BaseDemoFragment() { - private val config = App.instance.defaultConfig - private val initializer = Initializer(App.instance, host = config.host, port = config.port) - private val birthday = config.loadBirthday() - private lateinit var synchronizer: Synchronizer - private lateinit var keyManager: SampleStorageBridge private lateinit var amountInput: TextView private lateinit var addressInput: TextView + // in a normal app, this would be stored securely with the trusted execution environment (TEE) + // but since this is a demo, we'll derive it on the fly + private lateinit var spendingKey: String + + + /** + * Initialize the required values that would normally live outside the demo but are repeated + * here for completeness so that each demo file can serve as a standalone example. + */ + private fun setup() { + App.instance.defaultConfig.let { config -> + VkInitializer(App.instance) { + import(config.seed, config.birthdayHeight) + }.let { initializer -> + synchronizer = Synchronizer(initializer) + } + spendingKey = DerivationTool.deriveSpendingKeys(config.seed).first() + } + } // // Observable properties (done without livedata or flows for simplicity) @@ -57,52 +73,22 @@ class SendFragment : BaseDemoFragment() { } - // - // BaseDemoFragment overrides - // - - override fun inflateBinding(layoutInflater: LayoutInflater): FragmentSendBinding = - FragmentSendBinding.inflate(layoutInflater) - - fun resetInBackground() { - val spendingKeys = initializer.new(config.seed, birthday) - keyManager = SampleStorageBridge().securelyStorePrivateKey(spendingKeys[0]) - synchronizer = Synchronizer(initializer) - } - - // STARTING POINT - fun onResetComplete() { - initSendUi() - startSynchronizer() - monitorChanges() - } - - fun onClear() { - synchronizer.stop() - initializer.clear() - } - - // // Private functions // private fun initSendUi() { - amountInput = binding.root.findViewById(R.id.input_amount).apply { - text = config.sendAmount.toString() - } - addressInput = binding.root.findViewById(R.id.input_address).apply { - text = config.toAddress + App.instance.defaultConfig.let { config -> + amountInput = binding.inputAmount.apply { + setText(config.sendAmount.toZecString()) + } + addressInput = binding.inputAddress.apply { + setText(config.toAddress) + } } binding.buttonSend.setOnClickListener(::onSend) } - private fun startSynchronizer() { - lifecycleScope.apply { - synchronizer.start(this) - } - } - private fun monitorChanges() { synchronizer.status.collectWith(lifecycleScope, ::onStatus) synchronizer.progress.collectWith(lifecycleScope, ::onProgress) @@ -110,6 +96,11 @@ class SendFragment : BaseDemoFragment() { synchronizer.balances.collectWith(lifecycleScope, ::onBalance) } + + // + // Change listeners + // + private fun onStatus(status: Synchronizer.Status) { binding.textStatus.text = "Status: $status" isSyncing = status != Synchronizer.Status.SYNCED @@ -148,11 +139,12 @@ class SendFragment : BaseDemoFragment() { val amount = amountInput.text.toString().toDouble().convertZecToZatoshi() val toAddress = addressInput.text.toString().trim() synchronizer.sendToAddress( - keyManager.key, + spendingKey, amount, toAddress, - "Demo App Funds" + "Funds from Demo App" ).collectWith(lifecycleScope, ::onPendingTxUpdated) + mainActivity()?.hideKeyboard() } private fun onPendingTxUpdated(pendingTransaction: PendingTransaction?) { @@ -197,4 +189,33 @@ class SendFragment : BaseDemoFragment() { binding.textInfo.text = "Active Transaction:" } + + // + // Android Lifecycle overrides + // + + override fun onActivityCreated(savedInstanceState: Bundle?) { + super.onActivityCreated(savedInstanceState) + setup() + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + initSendUi() + } + + override fun onResume() { + super.onResume() + // the lifecycleScope is used to dispose of the synchronizer when the fragment dies + synchronizer.start(lifecycleScope) + monitorChanges() + } + + // + // BaseDemoFragment overrides + // + + override fun inflateBinding(layoutInflater: LayoutInflater): FragmentSendBinding = + FragmentSendBinding.inflate(layoutInflater) + } diff --git a/samples/demo-app/app/src/main/java/cash/z/ecc/android/sdk/demoapp/util/Ext.kt b/samples/demo-app/app/src/main/java/cash/z/ecc/android/sdk/demoapp/util/Ext.kt index 4043e8fd..4b017dfb 100644 --- a/samples/demo-app/app/src/main/java/cash/z/ecc/android/sdk/demoapp/util/Ext.kt +++ b/samples/demo-app/app/src/main/java/cash/z/ecc/android/sdk/demoapp/util/Ext.kt @@ -1,11 +1,46 @@ package cash.z.ecc.android.sdk.demoapp.util +import android.text.format.DateUtils import androidx.fragment.app.Fragment +import cash.z.ecc.android.sdk.demoapp.App import cash.z.ecc.android.sdk.demoapp.MainActivity +import cash.z.wallet.sdk.rpc.CompactFormats /** * Lazy extensions to make demo life easier. */ -fun Fragment.mainActivity() = context as? MainActivity \ No newline at end of file +fun Fragment.mainActivity() = context as? MainActivity + + + + +/** + * Add locale-specific commas to a number, if it exists. + */ +fun Number?.withCommas() = this?.let { "%,d".format(it) } ?: "Unknown" + +/** + * Convert date time in seconds to relative time like (4 days ago). + */ +fun Int?.toRelativeTime() = + this?.let { timeInSeconds -> + DateUtils.getRelativeDateTimeString( + App.instance, + timeInSeconds * 1000L, + DateUtils.SECOND_IN_MILLIS, + DateUtils.WEEK_IN_MILLIS, + DateUtils.FORMAT_SHOW_DATE or DateUtils.FORMAT_SHOW_YEAR or DateUtils.FORMAT_ABBREV_MONTH + ).toString() + } ?: "Unknown" + + +fun List?.toHtml() = + this.takeUnless { it.isNullOrEmpty() }?.let { txs -> + buildString { + append("
transactions (shielded INs / OUTs):") + txs.forEach { append("
  tx${it.index}: ${it.spendsCount} / ${it.outputsCount}") } + } + } ?: "" + diff --git a/samples/demo-app/app/src/main/res/layout/fragment_get_address.xml b/samples/demo-app/app/src/main/res/layout/fragment_get_address.xml index d53c147a..53ae43b3 100644 --- a/samples/demo-app/app/src/main/res/layout/fragment_get_address.xml +++ b/samples/demo-app/app/src/main/res/layout/fragment_get_address.xml @@ -10,11 +10,12 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:padding="16dp" - android:textSize="20sp" + android:textSize="18sp" android:text="loading address..." app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toTopOf="parent" /> + app:layout_constraintTop_toTopOf="parent" + app:layout_constraintVertical_bias="0.2"/> diff --git a/samples/demo-app/app/src/main/res/layout/fragment_get_block.xml b/samples/demo-app/app/src/main/res/layout/fragment_get_block.xml index bb819d6f..cc57b081 100644 --- a/samples/demo-app/app/src/main/res/layout/fragment_get_block.xml +++ b/samples/demo-app/app/src/main/res/layout/fragment_get_block.xml @@ -9,20 +9,26 @@ android:id="@+id/text_layout_block_height" android:layout_width="wrap_content" android:layout_height="wrap_content" + android:layout_marginEnd="16dp" + app:endIconMode="clear_text" app:layout_constraintBottom_toBottomOf="parent" - app:layout_constraintEnd_toStartOf="@id/button_apply" + app:layout_constraintEnd_toEndOf="parent" app:layout_constraintHorizontal_chainStyle="packed" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" - app:layout_constraintVertical_bias="0.2"> + app:layout_constraintVertical_bias="0.1"> @@ -31,21 +37,48 @@ android:id="@+id/button_apply" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:text="apply" - app:layout_constraintBottom_toBottomOf="@id/text_layout_block_height" - app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintStart_toEndOf="@id/text_layout_block_height" - app:layout_constraintTop_toTopOf="@id/text_layout_block_height" /> - - +