TalerUsbSession.kt
/*
* This file is part of GNU Taler
* (C) 2026 Taler Systems S.A.
*
* GNU Taler is free software; you can redistribute it and/or modify it under the
* terms of the GNU General Public License as published by the Free Software
* Foundation; either version 3, or (at your option) any later version.
*
* GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
* A PARTICULAR PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with
* GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
package net.taler.merchantpos.devices
internal data class TalerUsbDevice(
val id: String,
val permitted: Boolean,
)
internal interface TalerUsbTransport
{
/** Lists attached supported devices, including those still awaiting USB permission. */
fun devices(): List<TalerUsbDevice>
/** True only if the entire payload was transferred. An empty URL requests idle. */
fun write(
device: TalerUsbDevice,
payload: ByteArray,
): Boolean
}
enum class TalerUsbStatus
{
PermissionRequired,
Ready,
Failed
}
data class TalerUsbDeviceState(
val id: String,
val status: TalerUsbStatus,
)
/** Runs on one worker, so a late write can never overtake cancellation or the next order. */
internal class TalerUsbSession(
private val transport: TalerUsbTransport,
)
{
private val sent = mutableMapOf<String, String>()
fun invalidateTransfers()
{
sent.replaceAll { _, _ -> "\u0000" }
}
fun detached(id: String)
{
sent.remove(id)
}
fun synchronize(
enabled: Boolean,
uri: String?,
): List<TalerUsbDeviceState>
{
val devices = transport.devices()
sent.keys.retainAll(devices.map { it.id }.toSet())
val payload = if (enabled) uri.orEmpty() else ""
return devices.map { device ->
val status =
when
{
!device.permitted ->
{
sent.remove(device.id)
TalerUsbStatus.PermissionRequired
}
// Never touch an unused device while the integration is off. Clear a
// device previously used by this session even when being switched off.
!enabled && device.id !in sent ->
{
TalerUsbStatus.Ready
}
sent[device.id] == payload ->
{
TalerUsbStatus.Ready
}
else ->
{
// Remember that it may have received data even on a short write,
// so disabling the integration still attempts to clear it.
sent[device.id] = "\u0000"
val bytes = payload.toByteArray(Charsets.UTF_8)
val success =
try
{
bytes.size <= MAX_PAYLOAD_BYTES && transport.write(device, bytes)
}
catch (_: RuntimeException)
{
false
}
if (success)
{
sent[device.id] = payload
TalerUsbStatus.Ready
}
else
{
TalerUsbStatus.Failed
}
}
}
TalerUsbDeviceState(device.id, status)
}
}
companion object
{
// Android versions before P limit USB transfers to 16 KiB.
const val MAX_PAYLOAD_BYTES = 16 * 1024
fun supports(
vendorId: Int,
productId: Int,
) = vendorId == 0xd00d && productId == 0x8f71
}
}