Implemented displaying FlickerCodes
This commit is contained in:
parent
4cdc573364
commit
ebbdd56418
|
@ -63,7 +63,7 @@ fun EnterTanDialogPreview_WithMultipleTanMedia() { // shows that dialog is reall
|
|||
fun EnterTanDialogPreview_Flickercode() {
|
||||
val tanMethods = listOf(TanMethod("chipTAN Flickercode", TanMethodType.ChipTanFlickercode, "902"))
|
||||
val bank = BankViewInfo("12345678", "SupiDupiNutzer", "Abzockbank", BankingGroup.Postbank)
|
||||
val tanChallenge = TanChallenge(TanChallengeType.Flickercode, ActionRequiringTan.GetAccountInfo, "Geben Sie die TAN ein", tanMethods.first().identifier, tanMethods, bank = bank, flickerCode = FlickerCode("", ""))
|
||||
val tanChallenge = TanChallenge(TanChallengeType.Flickercode, ActionRequiringTan.GetAccountInfo, "Geben Sie die TAN ein", tanMethods.first().identifier, tanMethods, bank = bank, flickerCode = FlickerCode("100880077104", "0604800771040F"))
|
||||
|
||||
EnterTanDialog(TanChallengeReceived(tanChallenge) { }) { }
|
||||
}
|
|
@ -0,0 +1,46 @@
|
|||
package net.codinux.banking.ui.composables.tan
|
||||
|
||||
import androidx.compose.foundation.Canvas
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.Path
|
||||
import androidx.compose.ui.graphics.drawscope.Fill
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import net.codinux.banking.ui.service.tan.Bit
|
||||
|
||||
@Composable
|
||||
fun ChipTanFlickerCodeStripeView(stripe: Bit, width: Dp, showTanGeneratorMarker: Boolean = false) {
|
||||
Column(Modifier.width(width).fillMaxHeight()) {
|
||||
val markerHeight = width * 0.5f
|
||||
val triangleSize = markerHeight.value * LocalDensity.current.density
|
||||
|
||||
Column(Modifier.padding(bottom = 4.dp).width(width).height(markerHeight), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Bottom) {
|
||||
if (showTanGeneratorMarker) {
|
||||
val path = Path().apply {
|
||||
// Line to the top-right corner of the triangle
|
||||
lineTo(triangleSize, 0f)
|
||||
|
||||
// Line to the bottom-center point of the triangle
|
||||
lineTo(triangleSize / 2, triangleSize)
|
||||
|
||||
// Close the path (line back to the starting point)
|
||||
close()
|
||||
}
|
||||
|
||||
Canvas(modifier = Modifier.size(markerHeight)) {
|
||||
drawPath(path, Color.White, 1f, Fill)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Column(Modifier.fillMaxSize().background(if (stripe.isHigh) Color.White else Color.Black)) {
|
||||
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,133 @@
|
|||
package net.codinux.banking.ui.composables.tan
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material.Icon
|
||||
import androidx.compose.material.IconButton
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Pause
|
||||
import androidx.compose.material.icons.filled.PlayArrow
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import net.codinux.banking.client.model.tan.FlickerCode
|
||||
import net.codinux.banking.ui.service.tan.Bit
|
||||
import net.codinux.banking.ui.service.tan.FlickerCodeAnimator
|
||||
import net.codinux.banking.ui.service.tan.Step
|
||||
|
||||
|
||||
private const val FrequencyStepSize = 2
|
||||
|
||||
private val StripesHeightStepSize = 7.dp
|
||||
private val StripesWidthStepSize = 2.dp
|
||||
private val SpaceBetweenStripesStepSize = 1.dp
|
||||
|
||||
|
||||
@Composable
|
||||
fun ChipTanFlickerCodeView(flickerCode: FlickerCode) {
|
||||
|
||||
val animator = remember { FlickerCodeAnimator() }
|
||||
|
||||
var stripesHeight by remember { mutableStateOf(240.dp) }
|
||||
|
||||
var stripesWidth by remember { mutableStateOf(45.dp) }
|
||||
|
||||
var spaceBetweenStripes by remember { mutableStateOf(15.dp) }
|
||||
|
||||
var frequency by remember { mutableStateOf(FlickerCodeAnimator.DefaultFrequency) }
|
||||
|
||||
var isPaused by remember { mutableStateOf(false) }
|
||||
|
||||
var step by remember { mutableStateOf(Step(Bit.High, Bit.High, Bit.High, Bit.High, Bit.High)) }
|
||||
|
||||
|
||||
fun setSize(width: Dp, height: Dp, spaceBetween: Dp) {
|
||||
stripesWidth = width
|
||||
stripesHeight = height
|
||||
spaceBetweenStripes = spaceBetween
|
||||
}
|
||||
|
||||
fun decreaseSize() {
|
||||
if (spaceBetweenStripes - SpaceBetweenStripesStepSize > 0.dp) {
|
||||
setSize(
|
||||
stripesWidth - StripesWidthStepSize,
|
||||
stripesHeight - StripesHeightStepSize,
|
||||
spaceBetweenStripes - SpaceBetweenStripesStepSize
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun increaseSize() { // set also an upper limit to size?
|
||||
setSize(
|
||||
stripesWidth + StripesWidthStepSize,
|
||||
stripesHeight + StripesHeightStepSize,
|
||||
spaceBetweenStripes + SpaceBetweenStripesStepSize
|
||||
)
|
||||
}
|
||||
|
||||
fun toggleIsPaused() {
|
||||
isPaused = !isPaused
|
||||
|
||||
if (isPaused) {
|
||||
animator.pause()
|
||||
} else {
|
||||
animator.resume()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Column(Modifier.fillMaxWidth()) {
|
||||
Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.Center, verticalAlignment = Alignment.CenterVertically) {
|
||||
ImageSizeControls(true, true, { decreaseSize() }, { increaseSize() })
|
||||
|
||||
Spacer(Modifier.width(16.dp))
|
||||
|
||||
IconButton({ toggleIsPaused() }) {
|
||||
if (isPaused) {
|
||||
Icon(Icons.Filled.PlayArrow, "FlickerCode Animation wieder starten")
|
||||
} else {
|
||||
Icon(Icons.Filled.Pause, "FlickerCode Animation pausieren")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Row(Modifier.background(Color.Black).padding(vertical = 20.dp), verticalAlignment = Alignment.CenterVertically) {
|
||||
Row(Modifier.fillMaxWidth().height(stripesHeight).background(Color.Black), horizontalArrangement = Arrangement.Center, verticalAlignment = Alignment.CenterVertically) {
|
||||
ChipTanFlickerCodeStripeView(step.bit1, stripesWidth, true)
|
||||
|
||||
Spacer(Modifier.width(spaceBetweenStripes))
|
||||
|
||||
ChipTanFlickerCodeStripeView(step.bit2, stripesWidth)
|
||||
|
||||
Spacer(Modifier.width(spaceBetweenStripes))
|
||||
|
||||
ChipTanFlickerCodeStripeView(step.bit3, stripesWidth)
|
||||
|
||||
Spacer(Modifier.width(spaceBetweenStripes))
|
||||
|
||||
ChipTanFlickerCodeStripeView(step.bit4, stripesWidth)
|
||||
|
||||
Spacer(Modifier.width(spaceBetweenStripes))
|
||||
|
||||
ChipTanFlickerCodeStripeView(step.bit5, stripesWidth, true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
DisposableEffect(animator) {
|
||||
animator.setFrequency(frequency)
|
||||
|
||||
animator.animateFlickerCode(flickerCode) {
|
||||
step = it
|
||||
}
|
||||
|
||||
onDispose {
|
||||
animator.stop()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,32 @@
|
|||
package net.codinux.banking.ui.composables.tan
|
||||
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.ZoomIn
|
||||
import androidx.compose.material.icons.filled.ZoomOut
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
@Composable
|
||||
fun ImageSizeControls(decreaseEnabled: Boolean, increaseEnabled: Boolean, onDecreaseImageSize: () -> Unit, onIncreaseImageSize: () -> Unit) {
|
||||
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Text("Größe")
|
||||
|
||||
Spacer(Modifier.width(6.dp))
|
||||
|
||||
TextButton({ onDecreaseImageSize() }, enabled = decreaseEnabled, modifier = Modifier.width(48.dp), colors = ButtonDefaults.buttonColors(backgroundColor = Color.Transparent)) {
|
||||
Icon(Icons.Filled.ZoomOut, contentDescription = "Bild verkleiner", Modifier.size(28.dp))
|
||||
}
|
||||
|
||||
Spacer(Modifier.width(6.dp))
|
||||
|
||||
TextButton({ onIncreaseImageSize() }, enabled = increaseEnabled, modifier = Modifier.width(48.dp), colors = ButtonDefaults.buttonColors(backgroundColor = Color.Transparent)) {
|
||||
Icon(Icons.Filled.ZoomIn, contentDescription = "Bild vergrößern", Modifier.size(28.dp))
|
||||
}
|
||||
}
|
||||
}
|
|
@ -4,15 +4,11 @@ import androidx.compose.foundation.Image
|
|||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.ZoomIn
|
||||
import androidx.compose.material.icons.filled.ZoomOut
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.focusRequester
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
@ -20,6 +16,8 @@ import kotlinx.datetime.TimeZone
|
|||
import kotlinx.datetime.toLocalDateTime
|
||||
import net.codinux.banking.client.model.tan.*
|
||||
import net.codinux.banking.ui.composables.BankIcon
|
||||
import net.codinux.banking.ui.composables.tan.ChipTanFlickerCodeView
|
||||
import net.codinux.banking.ui.composables.tan.ImageSizeControls
|
||||
import net.codinux.banking.ui.config.Colors
|
||||
import net.codinux.banking.ui.config.DI
|
||||
import net.codinux.banking.ui.config.Internationalization
|
||||
|
@ -29,7 +27,6 @@ import net.codinux.banking.ui.forms.Select
|
|||
import net.codinux.banking.ui.model.TanChallengeReceived
|
||||
import net.codinux.banking.ui.model.error.ErroneousAction
|
||||
import net.codinux.banking.ui.service.createImageBitmap
|
||||
import net.codinux.log.Log
|
||||
import kotlin.io.encoding.Base64
|
||||
import kotlin.io.encoding.ExperimentalEncodingApi
|
||||
|
||||
|
@ -114,19 +111,9 @@ fun EnterTanDialog(tanChallengeReceived: TanChallengeReceived, onDismiss: () ->
|
|||
"TAN Verfahren",
|
||||
challenge.availableTanMethods.sortedBy { it.identifier },
|
||||
challenge.selectedTanMethod,
|
||||
{ tanMethod ->
|
||||
if (tanMethod.type != TanMethodType.ChipTanFlickercode) {
|
||||
tanChallengeReceived.callback(EnterTanResult(null, tanMethod))
|
||||
}
|
||||
},
|
||||
{ tanMethod -> tanChallengeReceived.callback(EnterTanResult(null, tanMethod)) },
|
||||
{ it.displayName }
|
||||
) { tanMethod ->
|
||||
if (tanMethod.type == TanMethodType.ChipTanFlickercode) {
|
||||
Text(tanMethod.displayName + " (noch nicht implementiert)", color = MaterialTheme.colors.onSurface.copy(ContentAlpha.disabled))
|
||||
} else {
|
||||
Text(tanMethod.displayName)
|
||||
}
|
||||
}
|
||||
) { tanMethod -> Text(tanMethod.displayName) }
|
||||
}
|
||||
|
||||
if (challenge.availableTanMedia.isNotEmpty()) {
|
||||
|
@ -148,9 +135,8 @@ fun EnterTanDialog(tanChallengeReceived: TanChallengeReceived, onDismiss: () ->
|
|||
|
||||
if (challenge.tanImage != null || challenge.flickerCode != null) {
|
||||
Column(Modifier.fillMaxWidth().padding(top = 6.dp)) {
|
||||
if (challenge.flickerCode != null) {
|
||||
Text("Es tut uns Leid, für die TAN müsste ein Flickercode angezeigt werden, was wir noch nicht implementiert haben.")
|
||||
Text("Bitte wählen Sie ein anderes TAN Verfahren, z. B. chipTAN-QrCode oder manuelle TAN Eingabe wie chipTAN manuell.", Modifier.padding(top = 6.dp))
|
||||
challenge.flickerCode?.let { flickerCode ->
|
||||
ChipTanFlickerCodeView(flickerCode)
|
||||
}
|
||||
|
||||
challenge.tanImage?.let { tanImage ->
|
||||
|
@ -158,15 +144,7 @@ fun EnterTanDialog(tanChallengeReceived: TanChallengeReceived, onDismiss: () ->
|
|||
val imageBytes = Base64.decode(tanImage.imageBytesBase64)
|
||||
|
||||
Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.Center, verticalAlignment = Alignment.CenterVertically) {
|
||||
Text("Größe")
|
||||
Spacer(Modifier.width(6.dp))
|
||||
TextButton({ tanImageHeight -= 25}, enabled = tanImageHeight > minTanImageHeight, modifier = Modifier.width(48.dp), colors = ButtonDefaults.buttonColors(backgroundColor = Color.Transparent)) {
|
||||
Icon(Icons.Filled.ZoomOut, contentDescription = "Bild mit enkodierter TAN verkleiner", Modifier.size(28.dp))
|
||||
}
|
||||
Spacer(Modifier.width(6.dp))
|
||||
TextButton({ tanImageHeight += 25}, enabled = tanImageHeight < maxTanImageHeight, modifier = Modifier.width(48.dp), colors = ButtonDefaults.buttonColors(backgroundColor = Color.Transparent)) {
|
||||
Icon(Icons.Filled.ZoomIn, contentDescription = "Bild mit enkodierter TAN vergrößern", Modifier.size(28.dp))
|
||||
}
|
||||
ImageSizeControls(tanImageHeight > minTanImageHeight, tanImageHeight < maxTanImageHeight, { tanImageHeight -= 25 }) { tanImageHeight += 25 }
|
||||
}
|
||||
|
||||
Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.Center, verticalAlignment = Alignment.CenterVertically) {
|
||||
|
|
|
@ -0,0 +1,28 @@
|
|||
package net.codinux.banking.ui.service.tan
|
||||
|
||||
enum class Bit(val value: Int) {
|
||||
|
||||
Low(0),
|
||||
|
||||
High(1);
|
||||
|
||||
|
||||
val isHigh: Boolean
|
||||
get() = this == High
|
||||
|
||||
val isLow: Boolean = !!! this.isHigh
|
||||
|
||||
fun invert(): Bit {
|
||||
if (this == High) {
|
||||
return Low
|
||||
}
|
||||
|
||||
return High
|
||||
}
|
||||
|
||||
|
||||
override fun toString(): String {
|
||||
return "$value"
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,85 @@
|
|||
package net.codinux.banking.ui.service.tan
|
||||
|
||||
import kotlinx.coroutines.*
|
||||
import net.codinux.banking.client.model.tan.FlickerCode
|
||||
import net.codinux.log.logger
|
||||
import kotlin.concurrent.Volatile
|
||||
|
||||
class FlickerCodeAnimator {
|
||||
|
||||
companion object {
|
||||
const val MinFrequency = 2
|
||||
const val MaxFrequency = 40
|
||||
const val DefaultFrequency = 30
|
||||
}
|
||||
|
||||
|
||||
@Volatile
|
||||
private var currentFrequency: Int = DefaultFrequency
|
||||
|
||||
@Volatile
|
||||
private var isPaused = false
|
||||
|
||||
private var animationJob: Job? = null
|
||||
|
||||
private val log by logger()
|
||||
|
||||
|
||||
|
||||
fun animateFlickerCode(flickerCode: FlickerCode, showStep: (Step) -> Unit) { // TODO: find better coroutine scope
|
||||
stop() // stop may still running previous animation
|
||||
|
||||
animationJob = GlobalScope.launch(Dispatchers.Default) {
|
||||
val steps = FlickerCodeStepsCalculator().calculateSteps(flickerCode.parsedDataSet)
|
||||
|
||||
calculateAnimation(steps, showStep)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun calculateAnimation(steps: List<Step>, showStep: (Step) -> Unit) {
|
||||
var currentStepIndex = 0
|
||||
|
||||
while (true) {
|
||||
if (isPaused == false) {
|
||||
val nextStep = steps[currentStepIndex]
|
||||
|
||||
withContext(Dispatchers.Main) {
|
||||
showStep(nextStep)
|
||||
}
|
||||
|
||||
currentStepIndex++
|
||||
if (currentStepIndex >= steps.size) {
|
||||
currentStepIndex = 0 // all steps shown, start again from beginning
|
||||
}
|
||||
}
|
||||
|
||||
delay(1000L / currentFrequency)
|
||||
}
|
||||
}
|
||||
|
||||
fun pause() {
|
||||
this.isPaused = true
|
||||
}
|
||||
|
||||
fun resume() {
|
||||
this.isPaused = false
|
||||
}
|
||||
|
||||
fun stop() {
|
||||
try {
|
||||
animationJob?.cancel()
|
||||
|
||||
animationJob = null
|
||||
} catch (e: Exception) {
|
||||
log.warn(e) { "Could not stop animation job" }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fun setFrequency(frequency: Int) {
|
||||
if (frequency in MinFrequency..MaxFrequency) {
|
||||
currentFrequency = frequency
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,76 @@
|
|||
package net.codinux.banking.ui.service.tan
|
||||
|
||||
open class FlickerCodeStepsCalculator {
|
||||
|
||||
companion object {
|
||||
|
||||
val bits = mutableMapOf<Char, Step>()
|
||||
|
||||
init {
|
||||
/* bitfield: clock, bits 2^1, 2^2, 2^3, 2^4 */
|
||||
bits['0'] = Step(Bit.Low, Bit.Low, Bit.Low, Bit.Low, Bit.Low)
|
||||
bits['1'] = Step(Bit.Low, Bit.High, Bit.Low, Bit.Low, Bit.Low)
|
||||
bits['2'] = Step(Bit.Low, Bit.Low, Bit.High, Bit.Low, Bit.Low)
|
||||
bits['3'] = Step(Bit.Low, Bit.High, Bit.High, Bit.Low, Bit.Low)
|
||||
bits['4'] = Step(Bit.Low, Bit.Low, Bit.Low, Bit.High, Bit.Low)
|
||||
bits['5'] = Step(Bit.Low, Bit.High, Bit.Low, Bit.High, Bit.Low)
|
||||
bits['6'] = Step(Bit.Low, Bit.Low, Bit.High, Bit.High, Bit.Low)
|
||||
bits['7'] = Step(Bit.Low, Bit.High, Bit.High, Bit.High, Bit.Low)
|
||||
bits['8'] = Step(Bit.Low, Bit.Low, Bit.Low, Bit.Low, Bit.High)
|
||||
bits['9'] = Step(Bit.Low, Bit.High, Bit.Low, Bit.Low, Bit.High)
|
||||
bits['A'] = Step(Bit.Low, Bit.Low, Bit.High, Bit.Low, Bit.High)
|
||||
bits['B'] = Step(Bit.Low, Bit.High, Bit.High, Bit.Low, Bit.High)
|
||||
bits['C'] = Step(Bit.Low, Bit.Low, Bit.Low, Bit.High, Bit.High)
|
||||
bits['D'] = Step(Bit.Low, Bit.High, Bit.Low, Bit.High, Bit.High)
|
||||
bits['E'] = Step(Bit.Low, Bit.Low, Bit.High, Bit.High, Bit.High)
|
||||
bits['F'] = Step(Bit.Low, Bit.High, Bit.High, Bit.High, Bit.High)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
open fun calculateSteps(flickerCode: String): List<Step> {
|
||||
|
||||
val halfbyteid = ObjectHolder(0)
|
||||
val clock = ObjectHolder(Bit.High)
|
||||
val bitarray = mutableListOf<Step>()
|
||||
|
||||
|
||||
/* prepend synchronization identifier */
|
||||
var code = "0FFF" + flickerCode
|
||||
if (code.length % 2 != 0) {
|
||||
code += "F"
|
||||
}
|
||||
|
||||
for (i in 0 until code.length step 2) {
|
||||
bits[code[i + 1]]?.let { bitarray.add(it) }
|
||||
bits[code[i]]?.let { bitarray.add(it) }
|
||||
}
|
||||
|
||||
val steps = mutableListOf<Step>()
|
||||
|
||||
do {
|
||||
steps.add(calculateStep(halfbyteid, clock, bitarray))
|
||||
} while (halfbyteid.value > 0 || clock.value == Bit.Low)
|
||||
|
||||
return steps
|
||||
}
|
||||
|
||||
protected open fun calculateStep(halfbyteid: ObjectHolder<Int>, clock: ObjectHolder<Bit>, bitarray: List<Step>): Step {
|
||||
val step = Step(clock.value, bitarray[halfbyteid.value])
|
||||
|
||||
clock.value = clock.value.invert()
|
||||
|
||||
if (clock.value == Bit.High) {
|
||||
halfbyteid.value++
|
||||
|
||||
if (halfbyteid.value >= bitarray.size) {
|
||||
halfbyteid.value = 0
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return step
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
package net.codinux.banking.ui.service.tan
|
||||
|
||||
data class ObjectHolder<T>(var value: T)
|
|
@ -0,0 +1,16 @@
|
|||
package net.codinux.banking.ui.service.tan
|
||||
|
||||
open class Step(
|
||||
val bit1: Bit,
|
||||
val bit2: Bit,
|
||||
val bit3: Bit,
|
||||
val bit4: Bit,
|
||||
val bit5: Bit
|
||||
) {
|
||||
|
||||
constructor(clockBit: Bit, step: Step) : this(clockBit, step.bit2, step.bit3, step.bit4, step.bit5)
|
||||
|
||||
|
||||
override fun toString() = "${bit1}${bit2}${bit3}${bit4}$bit5"
|
||||
|
||||
}
|
Loading…
Reference in New Issue