taler-android

Android apps for GNU Taler (wallet, PoS, cashier)
Log | Files | Refs | README | LICENSE

NfcTotpWriter.kt (5958B)


      1 /*
      2  * This file is part of GNU Taler
      3  * (C) 2026 Taler Systems S.A.
      4  *
      5  * GNU Taler is free software; you can redistribute it and/or modify it under the
      6  * terms of the GNU General Public License as published by the Free Software
      7  * Foundation; either version 3, or (at your option) any later version.
      8  *
      9  * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
     10  * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
     11  * A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
     12  *
     13  * You should have received a copy of the GNU General Public License along with
     14  * GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
     15  */
     16 
     17 package net.taler.wallet.payment
     18 
     19 import android.app.Activity
     20 import android.nfc.NdefMessage
     21 import android.nfc.NdefRecord
     22 import android.nfc.NfcAdapter
     23 import android.nfc.tech.Ndef
     24 import android.nfc.tech.NfcA
     25 import android.widget.Toast
     26 import androidx.compose.foundation.layout.Column
     27 import androidx.compose.foundation.layout.Spacer
     28 import androidx.compose.foundation.layout.fillMaxWidth
     29 import androidx.compose.foundation.layout.height
     30 import androidx.compose.foundation.layout.padding
     31 import androidx.compose.material3.Button
     32 import androidx.compose.material3.ButtonDefaults
     33 import androidx.compose.material3.MaterialTheme
     34 import androidx.compose.material3.Text
     35 import androidx.compose.runtime.Composable
     36 import androidx.compose.runtime.DisposableEffect
     37 import androidx.compose.runtime.getValue
     38 import androidx.compose.runtime.mutableStateOf
     39 import androidx.compose.runtime.remember
     40 import androidx.compose.runtime.setValue
     41 import androidx.compose.ui.Alignment.Companion.CenterHorizontally
     42 import androidx.compose.ui.Modifier
     43 import androidx.compose.ui.platform.LocalContext
     44 import androidx.compose.ui.res.stringResource
     45 import androidx.compose.ui.text.font.FontFamily
     46 import androidx.compose.ui.unit.dp
     47 import net.taler.wallet.R
     48 import java.io.ByteArrayOutputStream
     49 
     50 private const val NFC_READER_FLAGS = NfcAdapter.FLAG_READER_NFC_A or NfcAdapter.FLAG_READER_NFC_V
     51 
     52 private fun formatTotpPayload(totpString: String): ByteArray {
     53     val codes = totpString.split("\n").mapNotNull { it.toUIntOrNull() }
     54     val out = ByteArrayOutputStream()
     55     out.write(0x42)
     56     for (code in codes) {
     57         val v = code.toInt()
     58         out.write(v shr 0 and 0xFF)
     59         out.write(v shr 8 and 0xFF)
     60         out.write(v shr 16 and 0xFF)
     61         out.write(v shr 24 and 0xFF)
     62     }
     63     return out.toByteArray()
     64 }
     65 
     66 @Composable
     67 fun TotpNfcWriter(
     68     totpString: String,
     69     modifier: Modifier = Modifier,
     70 ) {
     71     val context = LocalContext.current
     72     val activity = context as? Activity
     73     var isWriting by remember { mutableStateOf(false) }
     74 
     75     DisposableEffect(activity) {
     76         onDispose {
     77             if (isWriting && activity != null) {
     78                 NfcAdapter.getDefaultAdapter(context)?.disableReaderMode(activity)
     79             }
     80         }
     81     }
     82 
     83     val ndefMessage = remember(totpString) {
     84         val payload = formatTotpPayload(totpString)
     85         val record = NdefRecord(
     86             NdefRecord.TNF_WELL_KNOWN,
     87             "T".encodeToByteArray(),
     88             ByteArray(0),
     89             payload,
     90         )
     91         NdefMessage(arrayOf(record))
     92     }
     93 
     94     Column(modifier = modifier, horizontalAlignment = CenterHorizontally) {
     95         Text(
     96             modifier = Modifier.padding(top = 16.dp, start = 16.dp, end = 16.dp),
     97             text = stringResource(R.string.payment_confirmation_code),
     98             style = MaterialTheme.typography.bodyMedium,
     99         )
    100 
    101         Text(
    102             modifier = Modifier.padding(top = 8.dp, start = 16.dp, end = 16.dp, bottom = 8.dp),
    103             text = totpString,
    104             fontFamily = FontFamily.Monospace,
    105             fontSize = MaterialTheme.typography.titleLarge.fontSize,
    106         )
    107 
    108         Button(
    109             onClick = {
    110                 val nfcAdapter = NfcAdapter.getDefaultAdapter(context)
    111                 val act = activity
    112                 if (nfcAdapter == null || act == null) {
    113                     Toast.makeText(context, R.string.nfc_not_available, Toast.LENGTH_SHORT).show()
    114                     return@Button
    115                 }
    116                 isWriting = true
    117                 nfcAdapter.enableReaderMode(act, { tag ->
    118                     val ndef = Ndef.get(tag)
    119                     if (ndef != null) {
    120                         try {
    121                             ndef.connect()
    122                             ndef.writeNdefMessage(ndefMessage)
    123                             act.runOnUiThread {
    124                                 Toast.makeText(context, R.string.nfc_write_success, Toast.LENGTH_SHORT).show()
    125                             }
    126                         } catch (e: Exception) {
    127                             act.runOnUiThread {
    128                                 Toast.makeText(context, "${e.message}", Toast.LENGTH_SHORT).show()
    129                             }
    130                         } finally {
    131                             act.runOnUiThread {
    132                                 isWriting = false
    133                                 nfcAdapter.disableReaderMode(act)
    134                             }
    135                         }
    136                     } else {
    137                         act.runOnUiThread {
    138                             isWriting = false
    139                             nfcAdapter.disableReaderMode(act)
    140                             Toast.makeText(context, R.string.nfc_tag_not_ndef, Toast.LENGTH_SHORT).show()
    141                         }
    142                     }
    143                 }, NFC_READER_FLAGS, null)
    144             },
    145             modifier = Modifier.fillMaxWidth().padding(horizontal = 32.dp).padding(top = 4.dp),
    146             enabled = !isWriting,
    147         ) {
    148             Text(if (isWriting) stringResource(R.string.nfc_writing) else stringResource(R.string.nfc_write_button))
    149         }
    150 
    151         if (!isWriting) {
    152             Spacer(Modifier.height(ButtonDefaults.IconSpacing))
    153         }
    154     }
    155 }