CheckoutStore.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.checkout
import android.util.AtomicFile
import android.util.Log
import java.io.File
import java.io.FileNotFoundException
import java.io.IOException
import kotlinx.serialization.builtins.ListSerializer
import kotlinx.serialization.json.Json
/** Writes synchronously with AtomicFile so a crash preserves either the previous or the new checkout state. */
class CheckoutStore(
private val file: File,
)
{
private val atomicFile = AtomicFile(file)
private val json =
Json {
ignoreUnknownKeys = true
encodeDefaults = true
}
private val serializer = ListSerializer(Checkout.serializer())
@Synchronized
fun load(): List<Checkout>
{
val bytes =
try
{
atomicFile.readFully()
}
catch (e: FileNotFoundException)
{
// Only a genuinely absent store is empty. Android may also report an
// inaccessible file as "not found"; never replace that payment history.
val backup = File("${file.path}.bak")
if (file.exists() ||
backup.exists() ||
file.absoluteFile.parentFile?.canRead() != true)
{
throw e
}
return emptyList()
}
return try
{
json.decodeFromString(serializer, bytes.decodeToString())
}
catch (e: Exception)
{
// Unreadable records are set aside, not deleted: they describe real money.
val quarantine = File(file.parentFile, "${file.name}.unreadable-${System.currentTimeMillis()}")
if (!file.renameTo(quarantine))
{
throw IOException("Could not preserve unreadable checkout store", e)
}
Log.e(TAG, "Checkout store unreadable, kept as ${quarantine.name}", e)
emptyList()
}
}
@Synchronized
fun save(checkouts: List<Checkout>)
{
// Serialize before opening the transaction, so an encoding failure cannot
// leave an unfinished write or an open stream behind.
val bytes = json.encodeToString(serializer, checkouts).encodeToByteArray()
val stream = atomicFile.startWrite()
try
{
stream.write(bytes)
atomicFile.finishWrite(stream)
}
catch (e: IOException)
{
atomicFile.failWrite(stream)
throw e
}
}
private companion object
{
const val TAG = "taler-pos-checkout"
}
}