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 }