Implemented persisting AppSettings with BankingPersistence

This commit is contained in:
dankito 2020-09-29 05:43:08 +02:00
parent 88ba716639
commit 64aa7d007b
15 changed files with 257 additions and 20 deletions

View File

@ -27,7 +27,7 @@ open class LuceneBankingPersistence(
protected val indexFolder: File, protected val indexFolder: File,
databaseFolder: File, databaseFolder: File,
serializer: ISerializer = JacksonJsonSerializer() serializer: ISerializer = JacksonJsonSerializer()
) : BankingPersistenceJson(File(databaseFolder, "accounts.json"), serializer), IBankingPersistence { ) : BankingPersistenceJson(databaseFolder, serializer), IBankingPersistence {
companion object { companion object {

View File

@ -7,8 +7,11 @@ import net.dankito.banking.persistence.dao.*
import net.dankito.banking.persistence.model.* import net.dankito.banking.persistence.model.*
@Database(entities = [ Bank::class, BankAccount::class, AccountTransaction::class, TanMethod::class, TanMedium::class ], @Database(entities = [
version = 1, exportSchema = false) Bank::class, BankAccount::class, AccountTransaction::class,
TanMethod::class, TanMedium::class,
TanMethodSettings::class
], version = 1, exportSchema = false)
@TypeConverters(net.dankito.banking.persistence.TypeConverters::class) @TypeConverters(net.dankito.banking.persistence.TypeConverters::class)
abstract class BankingDatabase : RoomDatabase() { abstract class BankingDatabase : RoomDatabase() {
@ -22,4 +25,7 @@ abstract class BankingDatabase : RoomDatabase() {
abstract fun tanMediumDao(): TanMediumDao abstract fun tanMediumDao(): TanMediumDao
abstract fun tanMethodSettingsDao(): TanMethodSettingsDao
} }

View File

@ -10,6 +10,7 @@ import net.dankito.banking.search.TransactionParty
import net.dankito.banking.ui.model.IAccountTransaction import net.dankito.banking.ui.model.IAccountTransaction
import net.dankito.banking.ui.model.TypedBankAccount import net.dankito.banking.ui.model.TypedBankAccount
import net.dankito.banking.ui.model.TypedBankData import net.dankito.banking.ui.model.TypedBankData
import net.dankito.banking.ui.model.settings.AppSettings
import net.dankito.banking.ui.model.tan.MobilePhoneTanMedium import net.dankito.banking.ui.model.tan.MobilePhoneTanMedium
import net.dankito.banking.ui.model.tan.TanGeneratorTanMedium import net.dankito.banking.ui.model.tan.TanGeneratorTanMedium
import net.dankito.banking.util.persistence.doSaveUrlToFile import net.dankito.banking.util.persistence.doSaveUrlToFile
@ -20,6 +21,12 @@ import net.sqlcipher.database.SupportFactory
open class RoomBankingPersistence(applicationContext: Context, password: String? = null) : IBankingPersistence, ITransactionPartySearcher { open class RoomBankingPersistence(applicationContext: Context, password: String? = null) : IBankingPersistence, ITransactionPartySearcher {
companion object {
const val FlickerCodeTanMethodSettingsId = 1
const val QrCodeTanMethodSettingsId = 2
const val PhotoTanTanMethodSettingsId = 3
}
protected val db: BankingDatabase protected val db: BankingDatabase
init { init {
@ -149,6 +156,36 @@ open class RoomBankingPersistence(applicationContext: Context, password: String?
} }
override fun saveOrUpdateAppSettings(appSettings: AppSettings) {
saveOrUpdateTanMethodSettings(appSettings.flickerCodeSettings, FlickerCodeTanMethodSettingsId)
saveOrUpdateTanMethodSettings(appSettings.qrCodeSettings, QrCodeTanMethodSettingsId)
saveOrUpdateTanMethodSettings(appSettings.photoTanSettings, PhotoTanTanMethodSettingsId)
}
protected open fun saveOrUpdateTanMethodSettings(settings: net.dankito.banking.ui.model.settings.TanMethodSettings?, id: Int) {
settings?.let {
val settingsEntity = TanMethodSettings(id, it.width, it.height, it.space, it.frequency)
db.tanMethodSettingsDao().saveOrUpdate(settingsEntity)
}
}
override fun readPersistedAppSettings(): AppSettings? {
val tanMethodSettings = db.tanMethodSettingsDao().getAll()
val settings = AppSettings()
settings.flickerCodeSettings = findTanMethodSettings(FlickerCodeTanMethodSettingsId, tanMethodSettings)
settings.qrCodeSettings = findTanMethodSettings(QrCodeTanMethodSettingsId, tanMethodSettings)
settings.photoTanSettings = findTanMethodSettings(PhotoTanTanMethodSettingsId, tanMethodSettings)
return settings
}
protected open fun findTanMethodSettings(id: Int, settings: List<TanMethodSettings>): TanMethodSettings? {
return settings.firstOrNull { it.id == id }
}
override fun saveUrlToFile(url: String, file: File) { override fun saveUrlToFile(url: String, file: File) {
doSaveUrlToFile(url, file) doSaveUrlToFile(url, file)
} }

View File

@ -0,0 +1,13 @@
package net.dankito.banking.persistence.dao
import androidx.room.*
import net.dankito.banking.persistence.model.TanMethodSettings
@Dao
interface TanMethodSettingsDao : BaseDao<TanMethodSettings> {
@Query("SELECT * FROM TanMethodSettings")
fun getAll(): List<TanMethodSettings>
}

View File

@ -0,0 +1,21 @@
package net.dankito.banking.persistence.model
import androidx.room.Entity
import androidx.room.PrimaryKey
import net.dankito.banking.persistence.dao.BaseDao
import net.dankito.banking.ui.model.settings.TanMethodSettings
@Entity
open class TanMethodSettings(
@PrimaryKey
open var id: Int,
width: Int,
height: Int,
space: Int = -1,
frequency: Int = -1
) : TanMethodSettings(width, height, space, frequency) {
internal constructor() : this(BaseDao.IdNotSet.toInt(), 0, 0) // for object deserializers
}

View File

@ -2,18 +2,34 @@ package net.dankito.banking.persistence
import net.dankito.banking.persistence.model.BankDataEntity import net.dankito.banking.persistence.model.BankDataEntity
import net.dankito.banking.ui.model.* import net.dankito.banking.ui.model.*
import net.dankito.banking.ui.model.settings.AppSettings
import net.dankito.utils.multiplatform.File import net.dankito.utils.multiplatform.File
import net.dankito.banking.util.ISerializer import net.dankito.banking.util.ISerializer
import net.dankito.banking.util.persistence.doSaveUrlToFile import net.dankito.banking.util.persistence.doSaveUrlToFile
open class BankingPersistenceJson( open class BankingPersistenceJson(
protected val jsonFile: File, protected val databaseFolder: File,
protected val serializer: ISerializer protected val serializer: ISerializer
) : IBankingPersistence { ) : IBankingPersistence {
companion object {
const val BanksJsonFileName = "accounts.json"
const val AppSettingsJsonFileName = "app_settings.json"
}
protected val banksJsonFile: File
protected val appSettingsJsonFile: File
init { init {
jsonFile.absoluteFile.parentFile.mkdirs() databaseFolder.mkdirs()
banksJsonFile = File(databaseFolder, BanksJsonFileName)
appSettingsJsonFile = File(databaseFolder, AppSettingsJsonFileName)
} }
@ -26,7 +42,7 @@ open class BankingPersistenceJson(
} }
override fun readPersistedBanks(): List<TypedBankData> { override fun readPersistedBanks(): List<TypedBankData> {
return serializer.deserializeListOr(jsonFile, BankDataEntity::class).map { it as TypedBankData } return serializer.deserializeListOr(banksJsonFile, BankDataEntity::class).map { it as TypedBankData }
} }
@ -37,7 +53,16 @@ open class BankingPersistenceJson(
protected open fun saveAllBanks(allBanks: List<TypedBankData>) { protected open fun saveAllBanks(allBanks: List<TypedBankData>) {
serializer.serializeObject(allBanks, jsonFile) serializer.serializeObject(allBanks, banksJsonFile)
}
override fun saveOrUpdateAppSettings(appSettings: AppSettings) {
serializer.serializeObject(appSettings, appSettingsJsonFile)
}
override fun readPersistedAppSettings(): AppSettings? {
return serializer.deserializeObject(appSettingsJsonFile, AppSettings::class)
} }

View File

@ -49,11 +49,11 @@ class BankingPersistenceJsonTest {
} }
private val file = File(TestDataFolder, "test_accounts.json") private val file = File(TestDataFolder, BankingPersistenceJson.BanksJsonFileName)
private val serializer = JacksonJsonSerializer() private val serializer = JacksonJsonSerializer()
private val underTest = BankingPersistenceJson(file, serializer) private val underTest = BankingPersistenceJson(TestDataFolder, serializer)
@Test @Test

View File

@ -1,6 +1,7 @@
package net.dankito.banking.persistence package net.dankito.banking.persistence
import net.dankito.banking.ui.model.* import net.dankito.banking.ui.model.*
import net.dankito.banking.ui.model.settings.AppSettings
import net.dankito.utils.multiplatform.File import net.dankito.utils.multiplatform.File
@ -15,6 +16,12 @@ interface IBankingPersistence {
fun saveOrUpdateAccountTransactions(account: TypedBankAccount, transactions: List<IAccountTransaction>) fun saveOrUpdateAccountTransactions(account: TypedBankAccount, transactions: List<IAccountTransaction>)
fun saveOrUpdateAppSettings(appSettings: AppSettings)
fun readPersistedAppSettings(): AppSettings?
fun saveUrlToFile(url: String, file: File) fun saveUrlToFile(url: String, file: File)
} }

View File

@ -1,6 +1,7 @@
package net.dankito.banking.persistence package net.dankito.banking.persistence
import net.dankito.banking.ui.model.* import net.dankito.banking.ui.model.*
import net.dankito.banking.ui.model.settings.AppSettings
import net.dankito.utils.multiplatform.File import net.dankito.utils.multiplatform.File
@ -24,6 +25,15 @@ open class NoOpBankingPersistence : IBankingPersistence {
} }
override fun saveOrUpdateAppSettings(appSettings: AppSettings) {
}
override fun readPersistedAppSettings(): AppSettings? {
return null
}
override fun saveUrlToFile(url: String, file: File) { override fun saveUrlToFile(url: String, file: File) {
} }

View File

@ -1,12 +1,17 @@
package net.dankito.banking.ui.model.settings package net.dankito.banking.ui.model.settings
import net.dankito.utils.multiplatform.UUID
open class AppSettings( open class AppSettings(
var flickerCodeSettings: TanMethodSettings? = null, open var flickerCodeSettings: TanMethodSettings? = null,
var qrCodeSettings: TanMethodSettings? = null, open var qrCodeSettings: TanMethodSettings? = null,
var photoTanSettings: TanMethodSettings? = null open var photoTanSettings: TanMethodSettings? = null
) { ) {
internal constructor() : this(null, null, null) // for object deserializers internal constructor() : this(null, null, null) // for object deserializers
open var technicalId: String = UUID.random()
} }

View File

@ -1,5 +1,7 @@
package net.dankito.banking.ui.model.settings package net.dankito.banking.ui.model.settings
import net.dankito.utils.multiplatform.UUID
open class TanMethodSettings( open class TanMethodSettings(
var width: Int, var width: Int,
@ -11,6 +13,9 @@ open class TanMethodSettings(
internal constructor() : this(0, 0) // for object deserializers internal constructor() : this(0, 0) // for object deserializers
open var technicalId: String = UUID.random()
override fun toString(): String { override fun toString(): String {
return "Size $width x $height, frequency $frequency" return "Size $width x $height, frequency $frequency"
} }

View File

@ -937,27 +937,29 @@ open class BankingPresenter(
} }
protected open fun persistAppSettings() { protected open fun persistAppSettings() {
asyncRunner.runAsync {
persistAppSettingsOffUiThread()
}
}
protected open fun persistAppSettingsOffUiThread() {
try { try {
serializer.serializeObject(appSettings, getAppSettingsFile()) persister.saveOrUpdateAppSettings(appSettings)
} catch (e: Exception) { } catch (e: Exception) {
log.error(e) { "Could not persist AppSettings to file ${getAppSettingsFile()}" } log.error(e) { "Could not persist AppSettings" }
} }
} }
protected open fun readAppSettings() { protected open fun readAppSettings() {
try { try {
serializer.deserializeObject(getAppSettingsFile(), AppSettings::class)?.let { persister.readPersistedAppSettings()?.let {
appSettings = it appSettings = it
} }
} catch (e: Exception) { } catch (e: Exception) {
log.error(e) { "Could not read AppSettings from file ${getAppSettingsFile()}" } log.error(e) { "Could not read AppSettings" }
} }
} }
protected open fun getAppSettingsFile(): File {
return File(dataFolder, "app_settings.json")
}
open fun addBanksChangedListener(listener: (List<TypedBankData>) -> Unit): Boolean { open fun addBanksChangedListener(listener: (List<TypedBankData>) -> Unit): Boolean {
return banksChangedListeners.add(listener) return banksChangedListeners.add(listener)

View File

@ -36,6 +36,11 @@
<attribute name="valueDate" attributeType="Date" usesScalarValueType="NO"/> <attribute name="valueDate" attributeType="Date" usesScalarValueType="NO"/>
<relationship name="account" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="PersistedBankAccount" inverseName="transactions" inverseEntity="PersistedBankAccount"/> <relationship name="account" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="PersistedBankAccount" inverseName="transactions" inverseEntity="PersistedBankAccount"/>
</entity> </entity>
<entity name="PersistedAppSettings" representedClassName="PersistedAppSettings" syncable="YES" codeGenerationType="class">
<relationship name="flickerCodeSettings" optional="YES" maxCount="1" deletionRule="Cascade" destinationEntity="PersistedTanMethodSettings"/>
<relationship name="photoTanSettings" optional="YES" maxCount="1" deletionRule="Cascade" destinationEntity="PersistedTanMethodSettings"/>
<relationship name="qrCodeSettings" optional="YES" maxCount="1" deletionRule="Cascade" destinationEntity="PersistedTanMethodSettings"/>
</entity>
<entity name="PersistedBankAccount" representedClassName="PersistedBankAccount" syncable="YES" codeGenerationType="class"> <entity name="PersistedBankAccount" representedClassName="PersistedBankAccount" syncable="YES" codeGenerationType="class">
<attribute name="accountHolderName" attributeType="String"/> <attribute name="accountHolderName" attributeType="String"/>
<attribute name="accountLimit" optional="YES" attributeType="String"/> <attribute name="accountLimit" optional="YES" attributeType="String"/>
@ -91,11 +96,19 @@
<attribute name="maxTanInputLength" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/> <attribute name="maxTanInputLength" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="type" attributeType="String"/> <attribute name="type" attributeType="String"/>
</entity> </entity>
<entity name="PersistedTanMethodSettings" representedClassName="PersistedTanMethodSettings" syncable="YES" codeGenerationType="class">
<attribute name="frequency" attributeType="Integer 32" defaultValueString="-1" usesScalarValueType="YES"/>
<attribute name="height" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="space" attributeType="Integer 32" defaultValueString="-1" usesScalarValueType="YES"/>
<attribute name="width" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
</entity>
<elements> <elements>
<element name="PersistedAccountTransaction" positionX="-36" positionY="45" width="128" height="553"/> <element name="PersistedAccountTransaction" positionX="-36" positionY="45" width="128" height="553"/>
<element name="PersistedBankAccount" positionX="-54" positionY="63" width="128" height="403"/> <element name="PersistedBankAccount" positionX="-54" positionY="63" width="128" height="403"/>
<element name="PersistedBankData" positionX="-63" positionY="-18" width="128" height="283"/> <element name="PersistedBankData" positionX="-63" positionY="-18" width="128" height="283"/>
<element name="PersistedTanMedium" positionX="-45" positionY="144" width="128" height="28"/> <element name="PersistedTanMedium" positionX="-45" positionY="144" width="128" height="28"/>
<element name="PersistedTanMethod" positionX="-54" positionY="135" width="128" height="118"/> <element name="PersistedTanMethod" positionX="-54" positionY="135" width="128" height="118"/>
<element name="PersistedTanMethodSettings" positionX="-54" positionY="135" width="128" height="103"/>
<element name="PersistedAppSettings" positionX="-45" positionY="144" width="128" height="88"/>
</elements> </elements>
</model> </model>

View File

@ -104,6 +104,49 @@ class CoreDataBankingPersistence: IBankingPersistence, ITransactionPartySearcher
} }
func saveOrUpdateAppSettings(appSettings: AppSettings) {
do {
let mapped = mapper.map(appSettings, context)
if appSettings.technicalId.isCoreDataId == false { // an unpersisted bank (but check should not be necessary)
context.insert(mapped)
}
try context.save()
appSettings.technicalId = mapped.objectIDAsString
if let flickerCodeSettingsId = mapped.flickerCodeSettings?.objectIDAsString {
appSettings.flickerCodeSettings?.technicalId = flickerCodeSettingsId
}
if let qrCodeSettingsId = mapped.qrCodeSettings?.objectIDAsString {
appSettings.qrCodeSettings?.technicalId = qrCodeSettingsId
}
if let photoTanSettingsId = mapped.photoTanSettings?.objectIDAsString {
appSettings.photoTanSettings?.technicalId = photoTanSettingsId
}
} catch {
NSLog("Could not save AppSettings \(appSettings): \(error)")
}
}
func readPersistedAppSettings() -> AppSettings? {
do {
let request: NSFetchRequest<PersistedAppSettings> = PersistedAppSettings.fetchRequest()
request.returnsObjectsAsFaults = false
let persistedSettings = try context.fetch(request)
if let settings = persistedSettings.first {
return mapper.map(settings)
}
} catch {
NSLog("Could not request AppSettings: \(error)")
}
return nil
}
func saveUrlToFile(url: String, file: URL) { func saveUrlToFile(url: String, file: URL) {
if let remoteUrl = URL.encoded(url) { if let remoteUrl = URL.encoded(url) {
if let fileData = try? Data(contentsOf: remoteUrl) { if let fileData = try? Data(contentsOf: remoteUrl) {

View File

@ -316,6 +316,56 @@ class Mapper {
} }
func map(_ settings: PersistedAppSettings) -> AppSettings {
let mapped = AppSettings(
flickerCodeSettings: map(settings.flickerCodeSettings),
qrCodeSettings: map(settings.qrCodeSettings),
photoTanSettings: map(settings.photoTanSettings))
mapped.technicalId = settings.objectIDAsString
return mapped
}
func map(_ settings: AppSettings, _ context: NSManagedObjectContext) -> PersistedAppSettings {
let mapped = context.objectByID(settings.technicalId) ?? PersistedAppSettings(context: context)
mapped.flickerCodeSettings = map(settings.flickerCodeSettings, context)
mapped.qrCodeSettings = map(settings.qrCodeSettings, context)
mapped.photoTanSettings = map(settings.photoTanSettings, context)
return mapped
}
func map(_ settings: PersistedTanMethodSettings?) -> TanMethodSettings? {
guard let settings = settings else {
return nil
}
let mapped = TanMethodSettings(width: settings.width, height: settings.height, space: settings.space, frequency: settings.frequency)
mapped.technicalId = settings.objectIDAsString
return mapped
}
func map(_ settings: TanMethodSettings?, _ context: NSManagedObjectContext) -> PersistedTanMethodSettings? {
guard let settings = settings else {
return nil
}
let mapped = context.objectByID(settings.technicalId) ?? PersistedTanMethodSettings(context: context)
mapped.width = settings.width
mapped.height = settings.height
mapped.space = settings.space
mapped.frequency = settings.frequency
return mapped
}
func map(_ date: Date?) -> CommonDate { func map(_ date: Date?) -> CommonDate {
if let date = date { if let date = date {
return CommonDate(date: date) return CommonDate(date: date)