Updated on 2026-08-14

This commit is contained in:
Tangem 2020-02-27 07:40:10 +00:00
commit b125063fab
14 changed files with 764 additions and 104 deletions

32
.gitignore vendored
View file

@ -1,20 +1,22 @@
*.iml
.gradle
/local.properties
/.idea/libraries
/.idea/modules.xml
/.idea/workspace.xml
.DS_Store
# Built application files
/build
/captures
.externalNativeBuild
.idea/vcs.xml
.idea/caches
.idea/dictionaries
.idea/runConfigurations.xml
.idea/encodings.xml
.idea/codeStyles/codeStyleConfig.xml
.idea/assetWizardSettings.xml
# Local configuration file (sdk path, etc)
local.properties
# Gradle generated files
.gradle
# User-specific configurations
.idea/caches/
.idea/libraries/
.idea/*.xml
# OS-specific files
.DS_Store
.DS_Store?
# fastlane files
**/fastlane/report.xml

View file

@ -176,6 +176,46 @@ class CardManager(
runTask(task, cardId, callback)
}
/**
* This command write some of User_Data, User_ProtectedData, User_Counter and User_ProtectedCounter fields.
* User_Data and User_ProtectedData are never changed or parsed by the executable code the Tangem COS.
* The App defines purpose of use, format and it's payload. For example, this field may contain cashed information
* from blockchain to accelerate preparing new transaction.
* User_Counter and User_ProtectedCounter are counters, that initial values can be set by App and increased on every signing
* of new transaction (on SIGN command that calculate new signatures). The App defines purpose of use.
* For example, this fields may contain blockchain nonce value.
*
* Writing of User_Counter and User_Data protected only by PIN1.
* User_ProtectedCounter and User_ProtectedData additionaly need PIN2 to confirmation.
*/
fun writeUserData(
cardId: String,
userData: ByteArray? = null,
userProtectedData: ByteArray? = null,
userCounter: Int? = null,
userProtectedCounter: Int? = null,
callback: (result: TaskEvent<WriteUserDataResponse>) -> Unit
) {
val writeUserDataCommand = WriteUserDataCommand(userData, userProtectedData, userCounter, userProtectedCounter)
val task = SingleCommandTask(writeUserDataCommand)
runTask(task, cardId, callback)
}
/**
* This command returns two up to 512-byte User_Data, User_Protected_Data and two counters User_Counter and
* User_Protected_Counter fields.
* User_Data and User_ProtectedData are never changed or parsed by the executable code the Tangem COS.
* The App defines purpose of use, format and it's payload. For example, this field may contain cashed information
* from blockchain to accelerate preparing new transaction.
* User_Counter and User_ProtectedCounter are counters, that initial values can be set by App and increased on every signing
* of new transaction (on SIGN command that calculate new signatures). The App defines purpose of use.
* For example, this fields may contain blockchain nonce value.
*/
fun readUserData(cardId: String, callback: (result: TaskEvent<ReadUserDataResponse>) -> Unit) {
val task = SingleCommandTask(ReadUserDataCommand())
runTask(task, cardId, callback)
}
/**
* This command will create a new wallet on the card having Empty state.
* A key pair WalletPublicKey / WalletPrivateKey is generated and securely stored in the card.
@ -210,8 +250,7 @@ class CardManager(
/**
*/
fun <T> runTask(task: Task<T>, cardId: String? = null,
callback: (result: TaskEvent<T>) -> Unit) {
fun <T> runTask(task: Task<T>, cardId: String? = null, callback: (result: TaskEvent<T>) -> Unit) {
if (isBusy) {
callback(TaskEvent.Completion(TaskError.Busy()))
return

View file

@ -0,0 +1,79 @@
package com.tangem.commands
import com.tangem.common.CardEnvironment
import com.tangem.common.apdu.CommandApdu
import com.tangem.common.apdu.Instruction
import com.tangem.common.apdu.ResponseApdu
import com.tangem.common.tlv.TlvBuilder
import com.tangem.common.tlv.TlvMapper
import com.tangem.common.tlv.TlvTag
import com.tangem.tasks.TaskError
/**
[REDACTED_AUTHOR]
*/
class ReadUserDataResponse(
/**
* CID, Unique Tangem card ID number.
*/
val cardId: String,
/**
* Data defined by user's App.
*/
val userData: ByteArray,
/**
* Data defined by user's App (confirmed by PIN2).
*/
val userProtectedData: ByteArray,
/**
* Counter initialized by user's App and increased on every signing of new transaction
*/
val userCounter: Int,
/**
* Counter initialized by user's App (confirmed by PIN2) and increased on every signing of new transaction
*/
val userProtectedCounter: Int
): CommandResponse
/**
* This command returns two up to 512-byte User_Data, User_Protected_Data and two counters User_Counter and
* User_Protected_Counter fields.
* User_Data and User_ProtectedData are never changed or parsed by the executable code the Tangem COS.
* The App defines purpose of use, format and it's payload. For example, this field may contain cashed information
* from blockchain to accelerate preparing new transaction.
* User_Counter and User_ProtectedCounter are counters, that initial values can be set by App and increased on every signing
* of new transaction (on SIGN command that calculate new signatures). The App defines purpose of use.
* For example, this fields may contain blockchain nonce value.
*/
class ReadUserDataCommand: CommandSerializer<ReadUserDataResponse>() {
override fun serialize(cardEnvironment: CardEnvironment): CommandApdu {
val builder = TlvBuilder()
builder.append(TlvTag.CardId, cardEnvironment.cardId)
builder.append(TlvTag.Pin, cardEnvironment.pin1)
return CommandApdu(Instruction.ReadUserData, builder.serialize())
}
override fun deserialize(cardEnvironment: CardEnvironment, responseApdu: ResponseApdu): ReadUserDataResponse? {
val tlvData = responseApdu.getTlvData() ?: return null
return try {
val mapper = TlvMapper(tlvData)
ReadUserDataResponse(
cardId = mapper.map(TlvTag.CardId),
userData = mapper.map(TlvTag.UserData),
userProtectedData = mapper.map(TlvTag.UserProtectedData),
userCounter = mapper.map(TlvTag.UserCounter),
userProtectedCounter = mapper.map(TlvTag.UserProtectedCounter)
)
} catch (exception: Exception) {
throw TaskError.SerializeCommandError()
}
}
}

View file

@ -0,0 +1,62 @@
package com.tangem.commands
import com.tangem.common.CardEnvironment
import com.tangem.common.apdu.CommandApdu
import com.tangem.common.apdu.Instruction
import com.tangem.common.apdu.ResponseApdu
import com.tangem.common.tlv.TlvBuilder
import com.tangem.common.tlv.TlvMapper
import com.tangem.common.tlv.TlvTag
import com.tangem.tasks.TaskError
/**
[REDACTED_AUTHOR]
*/
class WriteUserDataResponse(
/**
* CID, Unique Tangem card ID number.
*/
val cardId: String
): CommandResponse
/**
* This command write some of User_Data, User_ProtectedData, User_Counter and User_ProtectedCounter fields.
* User_Data and User_ProtectedData are never changed or parsed by the executable code the Tangem COS.
* The App defines purpose of use, format and it's payload. For example, this field may contain cashed information
* from blockchain to accelerate preparing new transaction.
* User_Counter and User_ProtectedCounter are counters, that initial values can be set by App and increased on every signing
* of new transaction (on SIGN command that calculate new signatures). The App defines purpose of use.
* For example, this fields may contain blockchain nonce value.
*
* Writing of User_Counter and User_Data protected only by PIN1.
* User_ProtectedCounter and User_ProtectedData additionaly need PIN2 to confirmation.
*/
class WriteUserDataCommand(private val userData: ByteArray? = null, private val userProtectedData: ByteArray? = null,
private val userCounter: Int? = null,
private val userProtectedCounter: Int? = null): CommandSerializer<WriteUserDataResponse>() {
override fun serialize(cardEnvironment: CardEnvironment): CommandApdu {
val builder = TlvBuilder()
builder.append(TlvTag.CardId, cardEnvironment.cardId)
builder.append(TlvTag.Pin, cardEnvironment.pin1)
builder.append(TlvTag.UserData, userData)
builder.append(TlvTag.UserCounter, userCounter)
builder.append(TlvTag.UserProtectedData, userProtectedData)
builder.append(TlvTag.UserProtectedCounter, userProtectedCounter)
if (userProtectedCounter != null || userProtectedData != null)
builder.append(TlvTag.Pin2, cardEnvironment.pin2)
return CommandApdu(Instruction.WriteUserData, builder.serialize())
}
override fun deserialize(cardEnvironment: CardEnvironment, responseApdu: ResponseApdu): WriteUserDataResponse? {
val tlvData = responseApdu.getTlvData() ?: return null
return try {
WriteUserDataResponse(TlvMapper(tlvData).map(TlvTag.CardId))
} catch (exception: Exception) {
throw TaskError.SerializeCommandError()
}
}
}

View file

@ -18,7 +18,9 @@ enum class Instruction(var code: Int) {
Sign(0xFB),
PurgeWallet(0xFC),
Activate(0xFE),
OpenSession(0xFF);
OpenSession(0xFF),
WriteUserData(0xE0),
ReadUserData(0xE1);
companion object {

View file

@ -94,7 +94,6 @@ enum class TlvTag(val code: Int) {
ProductMask(0x8A),
PaymentFlowVersion(0x54),
UserCounter(0x2C),
TokenSymbol(0xA0),
@ -107,7 +106,12 @@ enum class TlvTag(val code: Int) {
TerminalIsLinked(0x58),
TerminalPublicKey(0x5C),
TerminalTransactionSignature(0x57);
TerminalTransactionSignature(0x57),
UserData(0x2A),
UserProtectedData(0x2B),
UserCounter(0x2C),
UserProtectedCounter(0x2D);
/**
* @return [TlvValueType] associated with a [TlvTag]
@ -121,7 +125,7 @@ enum class TlvTag(val code: Int) {
MaxSignatures, PauseBeforePin2, RemainingSignatures,
SignedHashes, Health, TokenDecimal,
Offset, Size -> TlvValueType.Uint16
UserCounter, IssuerDataCounter -> TlvValueType.Uint32
UserCounter, UserProtectedCounter, IssuerDataCounter -> TlvValueType.Uint32
IsActivated, TerminalIsLinked -> TlvValueType.BoolValue
ManufactureDateTime -> TlvValueType.DateTime
ProductMask -> TlvValueType.ProductMask

View file

@ -16,6 +16,7 @@
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
@ -46,6 +47,32 @@
android:name="android.nfc.action.TECH_DISCOVERED"
android:resource="@xml/nfc_tech_filter" />
</activity>
<activity android:name=".TestUserDataActivity">
<intent-filter>
<action android:name="android.nfc.action.NDEF_DISCOVERED" />
<category android:name="android.intent.category.DEFAULT" />
<data
android:host="www.tangem.com"
android:scheme="http" />
<data
android:host="www.tangem.com"
android:scheme="https" />
<data
android:host="tangem.com"
android:scheme="http" />
<data
android:host="tangem.com"
android:scheme="https" />
</intent-filter>
<intent-filter>
<action android:name="android.nfc.action.TECH_DISCOVERED" />
</intent-filter>
<meta-data
android:name="android.nfc.action.TECH_DISCOVERED"
android:resource="@xml/nfc_tech_filter" />
</activity>
</application>
</manifest>

View file

@ -1,13 +1,9 @@
package com.tangem.tangemtest
import android.content.Intent
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import com.tangem.CardManager
import com.tangem.common.extensions.hexToBytes
import com.tangem.common.extensions.toByteArray
import com.tangem.common.extensions.toHexString
import com.tangem.crypto.CryptoUtils
import com.tangem.crypto.sign
import com.tangem.tangem_sdk_new.extensions.init
import com.tangem.tasks.ScanEvent
import com.tangem.tasks.TaskError
@ -154,6 +150,7 @@ class MainActivity : AppCompatActivity() {
}
}
}
btn_read_write_user_data?.setOnClickListener { startActivity(Intent(this, TestUserDataActivity::class.java)) }
}
private fun createSampleHashes(): Array<ByteArray> {

View file

@ -0,0 +1,180 @@
package com.tangem.tangemtest
import android.os.Bundle
import android.view.View
import android.widget.CompoundButton
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
import com.tangem.CardManager
import com.tangem.commands.ReadUserDataResponse
import com.tangem.commands.WriteUserDataResponse
import com.tangem.common.CardEnvironment
import com.tangem.tangem_sdk_new.extensions.init
import com.tangem.tasks.ScanEvent
import com.tangem.tasks.TaskError
import com.tangem.tasks.TaskEvent
import kotlinx.android.synthetic.main.activity_test_user_data.*
import java.nio.charset.StandardCharsets
/**
[REDACTED_AUTHOR]
*/
class TestUserDataActivity: AppCompatActivity() {
private lateinit var cardManager: CardManager
private lateinit var writeOptions: WriteOptions
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_test_user_data)
init()
initWriteOptions()
}
private fun init() {
cardManager = CardManager.init(this)
btn_scan?.setOnClickListener { _ ->
cardManager.scanCard { taskEvent ->
when (taskEvent) {
is TaskEvent.Event -> {
when (taskEvent.data) {
is ScanEvent.OnReadEvent -> {
// Handle returned card data
writeOptions.cardId = (taskEvent.data as ScanEvent.OnReadEvent).card.cardId
runOnUiThread { showReadWriteSection(true) }
}
is ScanEvent.OnVerifyEvent -> {
//Handle card verification
}
}
}
is TaskEvent.Completion -> {
if (taskEvent.error != null) {
if (taskEvent.error is TaskError.UserCancelled) {
// Handle case when user cancelled manually
}
// Handle other errors
}
// Handle completion
}
}
}
}
btn_write.setOnClickListener {
if (writeOptions.cardId == null) return@setOnClickListener
cardManager.writeUserData(
writeOptions.cardId !!,
writeOptions.userData,
writeOptions.userProtectedData,
writeOptions.userCounter,
writeOptions.userProtectedCounter
) {
when (it) {
is TaskEvent.Completion -> handleError(tv_write_result, it.error)
is TaskEvent.Event -> {
runOnUiThread {
val data = it.data as? WriteUserDataResponse
if (data == null) {
tv_write_result.text = "Response doesn't match"
return@runOnUiThread
}
tv_write_result?.text = "Success"
}
}
}
}
}
btn_read.setOnClickListener {
if (writeOptions.cardId == null) return@setOnClickListener
cardManager.readUserData(writeOptions.cardId !!) {
when (it) {
is TaskEvent.Completion -> handleError(tv_read_result, it.error)
is TaskEvent.Event -> {
runOnUiThread {
val data = it.data as? ReadUserDataResponse
if (data == null) {
tv_read_result.text = "Response doesn't match"
return@runOnUiThread
}
tv_read_result?.text = "Success"
writeOptions.userData = data.userData
writeOptions.userProtectedData = data.userProtectedData
writeOptions.userCounter = data.userCounter
writeOptions.userProtectedCounter = data.userProtectedCounter
tv_card_cid.text = data.cardId
tv_data.text = String(data.userData, StandardCharsets.US_ASCII)
tv_protected_data.text = String(data.userProtectedData, StandardCharsets.US_ASCII)
tv_counter.text = data.userCounter.toString()
tv_protected_counter.text = data.userProtectedCounter.toString()
}
}
}
}
}
}
private fun handleError(tv: TextView, error: TaskError?) {
val er = error ?: return
if (er is TaskError.UserCancelled) return
runOnUiThread { tv.text = er::class.simpleName }
}
private fun initWriteOptions() {
writeOptions = WriteOptions()
chb_with_ud.setOnCheckedChangeListener { buttonView, isChecked -> writeOptions.updateData(buttonView) }
chb_with_ud_protected.setOnCheckedChangeListener { buttonView, isChecked -> writeOptions.updateProtectedData(buttonView) }
chb_with_counter.setOnCheckedChangeListener { buttonView, isChecked -> writeOptions.updateCounter(buttonView) }
chb_with_protected_counter.setOnCheckedChangeListener { buttonView, isChecked -> writeOptions.updateProtectedCounter(buttonView) }
chb_with_pin2.setOnCheckedChangeListener { buttonView, isChecked -> writeOptions.updatePin2(buttonView) }
}
private fun showReadWriteSection(show: Boolean) {
val state = if (show) View.VISIBLE else View.GONE
cl_read_write.visibility = state
}
}
class WriteOptions {
var cardId: String? = null
var userData: ByteArray? = null
var userProtectedData: ByteArray? = null
var userCounter: Int? = null
var userProtectedCounter: Int? = null
var pin2: String? = null
fun updateData(chbx: CompoundButton) {
val value = "simple user data".toByteArray()
userData = if (chbx.isChecked) value else null
}
fun updateProtectedData(chbx: CompoundButton) {
val value = "protected user data".toByteArray()
userProtectedData = if (chbx.isChecked) value else null
}
fun updateCounter(chbx: CompoundButton) {
val value = if (userCounter == null) 0 else userCounter !! + 1
userCounter = if (chbx.isChecked) value else null
}
fun updateProtectedCounter(chbx: CompoundButton) {
val value = if (userProtectedCounter == null) 0 else userProtectedCounter !! + 1
userProtectedCounter = if (chbx.isChecked) value else null
}
fun updatePin2(chbx: CompoundButton) {
val value = CardEnvironment.DEFAULT_PIN2
pin2 = if (chbx.isChecked) value else null
}
}

View file

@ -1,101 +1,116 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
<ScrollView 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:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/container"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<TextView
android:paddingBottom="48dp"
android:layout_marginTop="48dp"
android:id="@+id/tv_card_cid"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="16dp"
android:layout_marginBottom="80dp"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
android:textAppearance="@style/TextAppearance.AppCompat.Large"/>
android:id="@+id/tv_card_cid"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="48dp"
android:layout_marginBottom="80dp"
android:padding="16dp"
android:paddingBottom="48dp"
android:textAppearance="@style/TextAppearance.AppCompat.Large"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<Button
android:id="@+id/btn_scan"
android:layout_width="200dp"
android:layout_height="wrap_content"
android:text="Read and verify card"
app:layout_constraintVertical_bias="0.3"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
android:id="@+id/btn_scan"
android:layout_width="200dp"
android:layout_height="wrap_content"
android:text="Read and verify card"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.25" />
<Button
android:id="@+id/btn_sign"
android:layout_marginTop="16dp"
android:layout_width="200dp"
android:layout_height="wrap_content"
android:text="Sign transaction"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@id/btn_scan"
android:enabled="false"/>
android:id="@+id/btn_sign"
android:layout_width="200dp"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:enabled="false"
android:text="Sign transaction"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@id/btn_scan" />
<Button
android:id="@+id/btn_read_issuer_data"
android:layout_marginTop="16dp"
android:layout_width="200dp"
android:layout_height="wrap_content"
android:text="Read issuer data"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@id/btn_sign"
android:enabled="false"/>
android:id="@+id/btn_read_issuer_data"
android:layout_width="200dp"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:enabled="false"
android:text="Read issuer data"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@id/btn_sign" />
<Button
android:id="@+id/btn_write_issuer_data"
android:layout_marginTop="16dp"
android:layout_width="200dp"
android:layout_height="wrap_content"
android:text="Write issuer data"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@id/btn_read_issuer_data"
android:enabled="false"/>
android:id="@+id/btn_write_issuer_data"
android:layout_width="200dp"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:enabled="false"
android:text="Write issuer data"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@id/btn_read_issuer_data" />
<Button
android:id="@+id/btn_read_issuer_extra_data"
android:layout_marginTop="16dp"
android:layout_width="200dp"
android:layout_height="wrap_content"
android:text="Read issuer extra data"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@id/btn_write_issuer_data"
android:enabled="false"/>
android:id="@+id/btn_read_issuer_extra_data"
android:layout_width="200dp"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:enabled="false"
android:text="Read issuer extra data"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@id/btn_write_issuer_data" />
<Button
android:id="@+id/btn_purge_wallet"
android:layout_marginTop="16dp"
android:layout_width="200dp"
android:layout_height="wrap_content"
android:text="Purge wallet"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@id/btn_read_issuer_extra_data"
android:enabled="false"/>
android:id="@+id/btn_purge_wallet"
android:layout_width="200dp"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:enabled="false"
android:text="Purge wallet"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@id/btn_read_issuer_extra_data" />
<Button
android:id="@+id/btn_create_wallet"
android:layout_marginTop="16dp"
android:layout_width="200dp"
android:layout_height="wrap_content"
android:text="Create wallet"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@id/btn_purge_wallet"
android:enabled="false"/>
android:id="@+id/btn_create_wallet"
android:layout_width="200dp"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:enabled="false"
android:text="Create wallet"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@id/btn_purge_wallet" />
<Button
android:id="@+id/btn_read_write_user_data"
android:layout_width="200dp"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="Read/Write user data"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/btn_create_wallet" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
</ScrollView>

View file

@ -0,0 +1,238 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.appcompat.widget.LinearLayoutCompat 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:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<Button
android:id="@+id/btn_scan"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginTop="16dp"
android:text="Scan card" />
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/cl_read_write"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:animateLayoutChanges="true"
android:orientation="vertical"
android:visibility="gone">
<LinearLayout
android:id="@+id/linearLayout"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:orientation="vertical"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<CheckBox
android:id="@+id/chb_with_ud"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="With User data" />
<CheckBox
android:id="@+id/chb_with_ud_protected"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="With User protected data" />
<CheckBox
android:id="@+id/chb_with_pin2"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="With Pin2" />
<CheckBox
android:id="@+id/chb_with_counter"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="With User counter" />
<CheckBox
android:id="@+id/chb_with_protected_counter"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="With User protected counter" />
</LinearLayout>
<Button
android:id="@+id/btn_write"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
android:text="Write user data"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/linearLayout" />
<androidx.constraintlayout.widget.Guideline
android:id="@+id/guideline2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintGuide_begin="205dp" />
<TextView
android:id="@+id/tv_write_result"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="Write result"
app:layout_constraintBottom_toBottomOf="@+id/btn_write"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="@+id/guideline2"
app:layout_constraintTop_toTopOf="@+id/btn_write" />
<include
android:id="@+id/m_delimiter_h"
layout="@layout/m_delimiter_h"
android:layout_width="0dp"
android:layout_height="@dimen/delimiter_size"
android:layout_marginTop="16dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/btn_write" />
<Button
android:id="@+id/btn_read"
android:layout_width="170dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
android:text="Read User Data"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/m_delimiter_h" />
<TextView
android:id="@+id/tv_read_result"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="Read result"
app:layout_constraintBottom_toBottomOf="@+id/btn_read"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="@+id/guideline2"
app:layout_constraintTop_toTopOf="@+id/btn_read" />
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginStart="20dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="16dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/btn_read"
app:layout_constraintVertical_bias="0.0">
<androidx.constraintlayout.widget.Guideline
android:id="@+id/guideline"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintGuide_begin="156dp" />
<TextView
android:id="@+id/textView6"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Card Id:"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/textView7"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="User data:"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/textView6" />
<TextView
android:id="@+id/textView8"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="User protected data:"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/textView7" />
<TextView
android:id="@+id/textView9"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="User counter:"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/textView8" />
<TextView
android:id="@+id/textView10"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="User protected counter:"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/textView9" />
<TextView
android:id="@+id/tv_card_cid"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintStart_toStartOf="@+id/guideline"
app:layout_constraintTop_toTopOf="parent"
tools:text="card id" />
<TextView
android:id="@+id/tv_data"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
app:layout_constraintStart_toStartOf="@+id/tv_card_cid"
app:layout_constraintTop_toBottomOf="@+id/textView6"
tools:text="data" />
<TextView
android:id="@+id/tv_protected_data"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
app:layout_constraintStart_toStartOf="@+id/tv_data"
app:layout_constraintTop_toBottomOf="@+id/textView7"
tools:text="protected data" />
<TextView
android:id="@+id/tv_counter"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
app:layout_constraintStart_toStartOf="@+id/tv_protected_data"
app:layout_constraintTop_toBottomOf="@+id/tv_protected_data"
tools:text="counter" />
<TextView
android:id="@+id/tv_protected_counter"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
app:layout_constraintStart_toStartOf="@+id/tv_counter"
app:layout_constraintTop_toBottomOf="@+id/tv_counter"
tools:text="protected counter" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.appcompat.widget.LinearLayoutCompat>

View file

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/m_delimiter_h"
android:layout_width="match_parent"
android:layout_height="@dimen/delimiter_size"
android:background="@color/delimiter"
android:orientation="vertical" />

View file

@ -3,4 +3,6 @@
<color name="colorPrimary">#027aff</color>
<color name="colorPrimaryDark">#027AFF</color>
<color name="colorAccent">#027AFF</color>
<color name="delimiter">#000000</color>
</resources>

View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<dimen name="delimiter_size">1dp</dimen>
</resources>