Everything functional before changing startup sequence

This commit is contained in:
Kevin Gorham 2019-02-12 19:09:06 -05:00 committed by Kevin Gorham
parent ba51a19b0b
commit ad708e69d5
11 changed files with 309 additions and 78 deletions

View File

@ -1,24 +1,16 @@
package cash.z.android.wallet.di.module package cash.z.android.wallet.di.module
import android.util.Log
import cash.z.android.wallet.BuildConfig import cash.z.android.wallet.BuildConfig
import cash.z.android.wallet.ZcashWalletApplication import cash.z.android.wallet.ZcashWalletApplication
import cash.z.android.wallet.di.module.Properties.CACHE_DB_NAME import cash.z.android.wallet.sample.SampleProperties
import cash.z.android.wallet.di.module.Properties.COMPACT_BLOCK_PORT import cash.z.android.wallet.sample.SampleProperties.COMPACT_BLOCK_PORT
import cash.z.android.wallet.di.module.Properties.COMPACT_BLOCK_SERVER import cash.z.android.wallet.sample.SampleProperties.COMPACT_BLOCK_SERVER
import cash.z.android.wallet.di.module.Properties.DATA_DB_NAME
import cash.z.android.wallet.di.module.Properties.SEED_PROVIDER
import cash.z.android.wallet.di.module.Properties.SPENDING_KEY_PROVIDER
import cash.z.wallet.sdk.data.* import cash.z.wallet.sdk.data.*
import cash.z.wallet.sdk.jni.JniConverter import cash.z.wallet.sdk.jni.JniConverter
import cash.z.wallet.sdk.secure.Wallet import cash.z.wallet.sdk.secure.Wallet
import dagger.Module import dagger.Module
import dagger.Provides import dagger.Provides
import okio.ByteString
import java.nio.charset.Charset
import javax.inject.Singleton import javax.inject.Singleton
import kotlin.properties.ReadOnlyProperty
import kotlin.reflect.KProperty
/** /**
* Module that contributes all the objects necessary for the synchronizer, which is basically everything that has * Module that contributes all the objects necessary for the synchronizer, which is basically everything that has
@ -43,21 +35,21 @@ internal object SynchronizerModule {
@Provides @Provides
@Singleton @Singleton
fun provideProcessor(application: ZcashWalletApplication, converter: JniConverter, twigger: Twig): CompactBlockProcessor { fun provideProcessor(application: ZcashWalletApplication, converter: JniConverter, twigger: Twig): CompactBlockProcessor {
return CompactBlockProcessor(application, converter, CACHE_DB_NAME, DATA_DB_NAME, logger = twigger) return CompactBlockProcessor(application, converter, SampleProperties.wallet.cacheDbName, SampleProperties.wallet.dataDbName, logger = twigger)
} }
@JvmStatic @JvmStatic
@Provides @Provides
@Singleton @Singleton
fun provideRepository(application: ZcashWalletApplication, converter: JniConverter, twigger: Twig): TransactionRepository { fun provideRepository(application: ZcashWalletApplication, converter: JniConverter, twigger: Twig): TransactionRepository {
return PollingTransactionRepository(application, DATA_DB_NAME, 10_000L, converter, twigger) return PollingTransactionRepository(application, SampleProperties.wallet.dataDbName, 10_000L, converter, twigger)
} }
@JvmStatic @JvmStatic
@Provides @Provides
@Singleton @Singleton
fun provideWallet(application: ZcashWalletApplication, converter: JniConverter, twigger: Twig): Wallet { fun provideWallet(application: ZcashWalletApplication, converter: JniConverter, twigger: Twig): Wallet {
return Wallet(converter, application.getDatabasePath(DATA_DB_NAME).absolutePath, "${application.cacheDir.absolutePath}/params", seedProvider = SEED_PROVIDER, spendingKeyProvider = SPENDING_KEY_PROVIDER, logger = twigger) return Wallet(converter, application.getDatabasePath(SampleProperties.wallet.dataDbName).absolutePath, "${application.cacheDir.absolutePath}/params", seedProvider = SampleProperties.wallet.seedProvider, spendingKeyProvider = SampleProperties.wallet.spendingKeyProvider, logger = twigger)
} }
@JvmStatic @JvmStatic
@ -91,55 +83,3 @@ internal object SynchronizerModule {
} }
} }
// TODO: load most of these properties in later, perhaps from settings
object Properties {
val COMPACT_BLOCK_SERVER = Servers.EMULATOR.host
const val COMPACT_BLOCK_PORT = 9067
const val CACHE_DB_NAME = "wallet_cache4821.db"
const val DATA_DB_NAME = "wallet_data4821.db"
val SEED_PROVIDER = SampleSeedProvider("dummyseed")
val SPENDING_KEY_PROVIDER = SampleSpendingKeyProvider("dummyseed")
}
enum class Servers(val host: String) {
EMULATOR("10.0.2.2"),
WLAN("10.0.0.26"),
BOLT_TESTNET("ec2-34-228-10-162.compute-1.amazonaws.com"),
ZCASH_TESTNET("lightwalletd.z.cash")
}
class SampleImportedSeedProvider(private val seedHex: String) : ReadOnlyProperty<Any?, ByteArray> {
override fun getValue(thisRef: Any?, property: KProperty<*>): ByteArray {
val bytes = ByteString.decodeHex(seedHex).toByteArray()
val stringBytes = String(bytes, Charset.forName("UTF-8"))
Log.e("TWIG-x", "byteString: $stringBytes")
return decodeHex(seedHex).also { Log.e("TWIG-x", "$it") }
}
fun decodeHex(hex: String): ByteArray {
val result = ByteArray(hex.length / 2)
for (i in result.indices) {
val d1 = decodeHexDigit(hex[i * 2]) shl 4
val d2 = decodeHexDigit(hex[i * 2 + 1])
result[i] = (d1 + d2).toByte()
}
return result
}
private fun decodeHexDigit(c: Char): Int {
if (c in '0'..'9') return c - '0'
if (c in 'a'..'f') return c - 'a' + 10
if (c in 'A'..'F') return c - 'A' + 10
throw IllegalArgumentException("Unexpected hex digit: $c")
}
}
class SampleSpendingKeyProvider2(private val seedValue: String) : ReadOnlyProperty<Any?, String> {
override fun getValue(thisRef: Any?, property: KProperty<*>): String {
// dynamically generating keyes, based on seed is out of scope for this sample
return "secret-extended-key-test1q0ks5jkcqqqqpqywf2mh5g2aw5smt252mqscphjr8svrqyvgtgss0av3jh37jc05pngstr6qcqu5x64zuk8entc97pfla68jd7g9fyhwv5l8pdey662qy3lr07w9yddpgwdlwt3tjgzhpszatyw90kpn4zs7feu5cudwnxcpts5k0za96xy0wt59nu7hg3ntalck7gwhn0nuyztmf8yceuhp0fn3wmrtr9mk9v6fhg8hwvsxp0thr4cn9r8pc0w3zh45czmnr7e3mrctlzaq7"
// return "secret-extended-key-test1q0f0urnmqqqqpqxlree5urprcmg9pdgvr2c88qhm862etv65eu84r9zwannpz4g88299xyhv7wf9xkecag653jlwwwyxrymfraqsnz8qfgds70qjammscxxyl7s7p9xz9w906epdpy8ztsjd7ez7phcd5vj7syx68sjskqs8j9lef2uuacghsh8puuvsy9u25pfvcdznta33qe6xh5lrlnhdkgymnpdug4jm6tpf803cad6tqa9c0ewq9l03fqxatevm97jmuv8u0ccxjews5"
}
}

View File

@ -0,0 +1,44 @@
package cash.z.android.wallet.sample
import cash.z.wallet.sdk.data.SampleSeedProvider
object AliceWallet {
const val name = "test.reference.alice"
val seedProvider = SampleSeedProvider(name)
val spendingKeyProvider = SampleSpendingKeySharedPref(name)
const val cacheDbName = "testalice_cache.db"
const val dataDbName = "testalice_data.db"
}
object BobWallet {
const val name = "test.reference.bob"
val seedProvider =
SampleSeedProvider(name)
val spendingKeyProvider = SampleSpendingKeySharedPref(name)
const val cacheDbName = "testalice_cache.db"
const val dataDbName = "testalice_data.db"
}
object MyWallet {
const val name = "mine"
val seedProvider =
SampleImportedSeedProvider("295761fce7fdc89fa1095259f5be6375c4a36f7a214767d668f9ef6e17aa6314")
val spendingKeyProvider = SampleSpendingKeySharedPref(name)
const val cacheDbName = "wallet_cache1202.db"
const val dataDbName = "wallet_data1202.db"
}
enum class Servers(val host: String) {
EMULATOR("10.0.2.2"),
WLAN("10.0.0.26"),
BOLT_TESTNET("ec2-34-228-10-162.compute-1.amazonaws.com"),
ZCASH_TESTNET("lightwalletd.z.cash")
}
// TODO: load most of these properties in later, perhaps from settings
object SampleProperties {
val COMPACT_BLOCK_SERVER = Servers.EMULATOR.host
const val COMPACT_BLOCK_PORT = 9067
val wallet = AliceWallet
}

View File

@ -0,0 +1,73 @@
package cash.z.android.wallet.sample
import android.preference.PreferenceManager
import android.util.Log
import cash.z.android.wallet.ZcashWalletApplication
import okio.ByteString
import java.nio.charset.Charset
import kotlin.properties.ReadOnlyProperty
import kotlin.properties.ReadWriteProperty
import kotlin.reflect.KProperty
import android.R.id.edit
import android.content.Context
import android.content.SharedPreferences
import java.lang.IllegalStateException
@Deprecated(message = InsecureWarning.message)
class SampleImportedSeedProvider(private val seedHex: String) : ReadOnlyProperty<Any?, ByteArray> {
override fun getValue(thisRef: Any?, property: KProperty<*>): ByteArray {
val bytes = ByteString.decodeHex(seedHex).toByteArray()
val stringBytes = String(bytes, Charset.forName("UTF-8"))
Log.e("TWIG-x", "byteString: $stringBytes")
return decodeHex(seedHex).also { Log.e("TWIG-x", "$it") }
}
fun decodeHex(hex: String): ByteArray {
val result = ByteArray(hex.length / 2)
for (i in result.indices) {
val d1 = decodeHexDigit(hex[i * 2]) shl 4
val d2 = decodeHexDigit(hex[i * 2 + 1])
result[i] = (d1 + d2).toByte()
}
return result
}
private fun decodeHexDigit(c: Char): Int {
if (c in '0'..'9') return c - '0'
if (c in 'a'..'f') return c - 'a' + 10
if (c in 'A'..'F') return c - 'A' + 10
throw IllegalArgumentException("Unexpected hex digit: $c")
}
}
@Deprecated(message = InsecureWarning.message)
class SampleSpendingKeySharedPref(private val fileName: String) : ReadWriteProperty<Any?, String> {
private fun getPrefs() = ZcashWalletApplication.instance
.getSharedPreferences(fileName, Context.MODE_PRIVATE)
override fun getValue(thisRef: Any?, property: KProperty<*>): String {
val preferences = getPrefs()
PreferenceManager.getDefaultSharedPreferences(ZcashWalletApplication.instance)
return preferences.getString("spending", null)
?: throw IllegalStateException(
"Spending key was not there when we needed it! Make sure it was saved " +
"during the first run of the app, when accounts were created!"
)
}
override fun setValue(thisRef: Any?, property: KProperty<*>, value: String) {
Log.e("TWIG", "Spending key is being stored")
val preferences = getPrefs()
val editor = preferences.edit()
editor.putString("spending", value)
editor.apply()
}
}
internal object InsecureWarning {
const val message = "Do not use this because it is insecure and only intended for test code and samples. " +
"Instead, use the Android Keystore system or a 3rd party library that leverages it."
}

View File

@ -3,21 +3,23 @@ package cash.z.android.wallet.ui.fragment
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import cash.z.android.wallet.R
import android.view.ViewGroup import android.view.ViewGroup
import androidx.databinding.DataBindingUtil import androidx.databinding.DataBindingUtil
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import cash.z.android.wallet.R
import cash.z.android.wallet.databinding.FragmentHistoryBinding
import cash.z.android.wallet.ui.adapter.TransactionAdapter
import cash.z.android.wallet.ui.presenter.HistoryPresenter import cash.z.android.wallet.ui.presenter.HistoryPresenter
import cash.z.android.wallet.ui.util.AlternatingRowColorDecoration
import cash.z.wallet.sdk.dao.WalletTransaction
import dagger.Module import dagger.Module
import dagger.android.ContributesAndroidInjector import dagger.android.ContributesAndroidInjector
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import cash.z.android.wallet.databinding.FragmentHistoryBinding
import cash.z.wallet.sdk.dao.WalletTransaction
class HistoryFragment : BaseFragment(), HistoryPresenter.HistoryView { class HistoryFragment : BaseFragment(), HistoryPresenter.HistoryView {
override val titleResId: Int get() = R.string.destination_title_history
lateinit var historyPresenter: HistoryPresenter lateinit var historyPresenter: HistoryPresenter
lateinit var binding: FragmentHistoryBinding lateinit var binding: FragmentHistoryBinding
@ -28,6 +30,25 @@ class HistoryFragment : BaseFragment(), HistoryPresenter.HistoryView {
.root .root
} }
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
mainActivity.let { mainActivity ->
mainActivity.setSupportActionBar(view.findViewById(R.id.toolbar))
mainActivity.supportActionBar?.setDisplayHomeAsUpEnabled(true)
mainActivity.supportActionBar?.setTitle(R.string.destination_title_history)
}
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
historyPresenter = HistoryPresenter(this, mainActivity.synchronizer)
binding.recyclerTransactionsHistory.apply {
layoutManager = LinearLayoutManager(activity, RecyclerView.VERTICAL, false)
adapter = TransactionAdapter()
addItemDecoration(AlternatingRowColorDecoration())
}
}
override fun onResume() { override fun onResume() {
super.onResume() super.onResume()
launch { launch {
@ -41,7 +62,8 @@ class HistoryFragment : BaseFragment(), HistoryPresenter.HistoryView {
} }
override fun setTransactions(transactions: List<WalletTransaction>) { override fun setTransactions(transactions: List<WalletTransaction>) {
} (binding.recyclerTransactionsHistory.adapter as TransactionAdapter).submitList(transactions)
}
} }
@Module @Module

View File

@ -6,6 +6,7 @@ import android.text.SpannableString
import android.text.Spanned import android.text.Spanned
import android.util.Log import android.util.Log
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.MotionEvent
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.view.animation.AccelerateInterpolator import android.view.animation.AccelerateInterpolator
@ -44,6 +45,7 @@ import kotlinx.android.synthetic.main.include_home_header.*
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlin.random.Random import kotlin.random.Random
import kotlin.random.nextLong import kotlin.random.nextLong
import kotlin.system.measureTimeMillis
/** /**
@ -117,7 +119,11 @@ class HomeFragment : BaseFragment(), HomePresenter.HomeView {
} }
launch { launch {
setFirstRunShown(mainActivity.synchronizer.isFirstRun()) Log.e("TWIG", "deciding whether to show first run")
val extraDelay = measureTimeMillis {
setFirstRunShown(mainActivity.synchronizer.isFirstRun() || mainActivity.synchronizer.isOutOfSync())
}
Log.e("TWIG", "done deciding whether to show first run in $extraDelay ms. Was that worth it? Or should we toggle a boolean in the application class?")
} }
header_active_transaction.visibility = View.GONE header_active_transaction.visibility = View.GONE
@ -161,7 +167,11 @@ class HomeFragment : BaseFragment(), HomePresenter.HomeView {
layoutManager = LinearLayoutManager(activity, RecyclerView.VERTICAL, false) layoutManager = LinearLayoutManager(activity, RecyclerView.VERTICAL, false)
adapter = TransactionAdapter().also { transactionAdapter = it } adapter = TransactionAdapter().also { transactionAdapter = it }
addItemDecoration(AlternatingRowColorDecoration()) addItemDecoration(AlternatingRowColorDecoration())
} }
// recycler_transactions.setOnClickListener {
// mainActivity.navController.navigate(R.id.nav_history_fragment)
// }
} }
@ -198,7 +208,7 @@ class HomeFragment : BaseFragment(), HomePresenter.HomeView {
// var hasEmptyViews = group_empty_view_items.visibility == View.VISIBLE // var hasEmptyViews = group_empty_view_items.visibility == View.VISIBLE
// if(!viewsInitialized) toggleViews(true) // if(!viewsInitialized) toggleViews(true)
// //
val message = if(progress >= 100) "Download complete! Processing blocks..." else "Downloading blocks ($progress%)" val message = if(progress >= 100) "Download complete! Processing blocks..." else "Downloading remaining blocks ($progress%)"
// text_wallet_message.text = message // text_wallet_message.text = message
if (snackbar == null && progress <= 50) { if (snackbar == null && progress <= 50) {
@ -209,7 +219,7 @@ class HomeFragment : BaseFragment(), HomePresenter.HomeView {
snackbar?.show() snackbar?.show()
} else { } else {
snackbar?.setText(message) snackbar?.setText(message)
if(progress == 100 && snackbar?.isShownOrQueued != true) snackbar?.show() if(snackbar?.isShownOrQueued != true) snackbar?.show()
} }
} }

View File

@ -67,6 +67,7 @@ class ReceiveFragment : BaseFragment() {
} }
private fun onAddressLoaded(address: String) { private fun onAddressLoaded(address: String) {
Log.e("TWIG", "onAddressLoaded: $address")
qrecycler.load(address) qrecycler.load(address)
.withQuietZoneSize(3) .withQuietZoneSize(3)
.withCorrectionLevel(QRecycler.CorrectionLevel.MEDIUM) .withCorrectionLevel(QRecycler.CorrectionLevel.MEDIUM)

View File

@ -0,0 +1,57 @@
package cash.z.android.wallet.ui.presenter
import android.util.Log
import cash.z.android.wallet.ui.presenter.Presenter.PresenterView
import cash.z.wallet.sdk.dao.WalletTransaction
import cash.z.wallet.sdk.data.ActiveSendTransaction
import cash.z.wallet.sdk.data.ActiveTransaction
import cash.z.wallet.sdk.data.Synchronizer
import cash.z.wallet.sdk.data.TransactionState
import kotlinx.coroutines.*
import kotlinx.coroutines.Dispatchers.Main
import kotlinx.coroutines.channels.ReceiveChannel
import kotlin.coroutines.CoroutineContext
class HistoryPresenter(
private val view: HistoryView,
private val synchronizer: Synchronizer
) : Presenter, CoroutineScope {
private val job = Job()
override val coroutineContext: CoroutineContext get() = Dispatchers.Main + job
interface HistoryView : PresenterView {
fun setTransactions(transactions: List<WalletTransaction>)
}
override suspend fun start() {
Log.e("@TWIG", "historyPresenter starting!")
launchTransactionBinder(synchronizer.repository.allTransactions())
}
override fun stop() {
Log.e("@TWIG", "historyPresenter stopping!")
job.cancel()
}
private fun CoroutineScope.launchTransactionBinder(channel: ReceiveChannel<List<WalletTransaction>>) = launch {
Log.e("@TWIG", "transaction binder starting!")
for (walletTransactionList in channel) {
Log.e("@TWIG", "received ${walletTransactionList.size} transactions for presenting")
bind(walletTransactionList)
}
Log.e("@TWIG", "transaction binder exiting!")
}
//
// View Callbacks on Main Thread
//
private fun bind(transactions: List<WalletTransaction>) {
Log.e("@TWIG", "binding ${transactions.size} walletTransactions")
view.setTransactions(transactions)
}
}

View File

@ -82,22 +82,23 @@ class HomePresenter(
// //
private fun bind(old: Long?, new: Long) = onMain { private fun bind(old: Long?, new: Long) = onMain {
Log.e("@TWIG-t", "binding balance of $new") Log.e("@TWIG-b", "binding balance of $new")
view.updateBalance(old ?: 0L, new) view.updateBalance(old ?: 0L, new)
} }
private fun bind(transactions: List<WalletTransaction>) = onMain { private fun bind(transactions: List<WalletTransaction>) = onMain {
Log.e("@TWIG-t", "binding ${transactions.size} walletTransactions") Log.e("@TWIG-b", "binding ${transactions.size} walletTransactions")
view.setTransactions(transactions) view.setTransactions(transactions)
} }
private fun bind(progress: Int) = onMain { private fun bind(progress: Int) = onMain {
Log.e("@TWIG-b", "binding progress of $progress")
view.showProgress(progress) view.showProgress(progress)
} }
private fun bind(activeTransactionMap: Map<ActiveTransaction, TransactionState>) = onMain { private fun bind(activeTransactionMap: Map<ActiveTransaction, TransactionState>) = onMain {
Log.e("@TWIG-v", "binding a.t. map of size ${activeTransactionMap.size}") Log.e("@TWIG-b", "binding a.t. map of size ${activeTransactionMap.size}")
if (activeTransactionMap.isNotEmpty()) view.setActiveTransactions(activeTransactionMap) if (activeTransactionMap.isNotEmpty()) view.setActiveTransactions(activeTransactionMap)
} }

View File

@ -0,0 +1,32 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<!-- Transactions -->
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recycler_transactions_history"
android:layout_width="0dp"
android:layout_height="0dp"
android:clipToPadding="false"
android:paddingBottom="72dp"
android:layout_margin="16dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/appbar"
tools:itemCount="15"
tools:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
tools:listitem="@layout/item_transaction_history"
tools:orientation="vertical" />
<include
layout="@layout/include_app_bar"
tools:ignore="MissingConstraints" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>

View File

@ -0,0 +1,51 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/container_transaction"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/home_transaction_item_background"
android:paddingEnd="8dp"
android:paddingRight="8dp"
android:elevation="1dp"
tools:ignore="RtlSymmetry">
<View
android:id="@+id/view_transaction_status"
android:layout_width="6dp"
android:layout_height="0dp"
android:layout_marginTop="1dp"
android:layout_marginBottom="1dp"
android:background="@color/colorPrimary"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@id/text_transaction_timestamp"
app:layout_constraintBottom_toBottomOf="@id/text_transaction_timestamp"/>
<TextView
android:id="@+id/text_transaction_timestamp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginLeft="8dp"
android:paddingBottom="8dp"
android:paddingTop="8dp"
tools:text="8/23 3:24pm"
android:textSize="@dimen/text_size_body_2"
android:textColor="@color/text_dark_dimmed"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toEndOf="@id/view_transaction_status"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/text_transaction_amount"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="+ 4.244"
android:textColor="@color/colorPrimary"
android:textSize="@dimen/text_size_body_2"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>