Implemented persisting data with Room

This commit is contained in:
dankito 2020-09-15 21:12:54 +02:00
parent 29dbe70983
commit 12e84109f3
41 changed files with 936 additions and 45 deletions

View File

@ -58,6 +58,8 @@ ext {
multiDexVersion = "2.0.1"
androidXCoreVersion = "1.3.1"
appCompatVersion = "1.1.0"
constraintLayoutVersion = "1.1.3"
@ -66,6 +68,8 @@ ext {
daggerVersion = "2.27"
roomVersion = "2.2.5"
/* JavaFX */

View File

@ -0,0 +1,37 @@
apply plugin: 'com.android.library'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
apply plugin: 'kotlin-kapt'
android {
compileSdkVersion androidCompileSdkVersion
buildToolsVersion androidBuildToolsVersion
defaultConfig {
minSdkVersion androidMinSdkVersion
targetSdkVersion androidTargetSdkVersion
versionName version
versionCode appVersionCode
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
consumerProguardFiles "consumer-rules.pro"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
}
dependencies {
implementation project(':BankingUiCommon')
implementation "androidx.room:room-runtime:$roomVersion"
kapt "androidx.room:room-compiler:$roomVersion"
implementation "androidx.room:room-ktx:$roomVersion"
}

View File

@ -0,0 +1,21 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile

View File

@ -0,0 +1,5 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="net.dankito.banking.persistence">
/
</manifest>

View File

@ -0,0 +1,25 @@
package net.dankito.banking.persistence
import androidx.room.Database
import androidx.room.RoomDatabase
import androidx.room.TypeConverters
import net.dankito.banking.persistence.dao.*
import net.dankito.banking.persistence.model.*
@Database(entities = [ Bank::class, BankAccount::class, AccountTransaction::class, TanProcedure::class, TanMedium::class ],
version = 1, exportSchema = false)
@TypeConverters(net.dankito.banking.persistence.TypeConverters::class)
abstract class BankingDatabase : RoomDatabase() {
abstract fun bankDao(): BankDao
abstract fun bankAccountDao(): BankAccountDao
abstract fun accountTransactionDao(): AccountTransactionDao
abstract fun tanProcedureDao(): TanProcedureDao
abstract fun tanMediumDao(): TanMediumDao
}

View File

@ -0,0 +1,134 @@
package net.dankito.banking.persistence
import android.content.Context
import androidx.room.Room
import net.dankito.banking.persistence.dao.saveOrUpdate
import net.dankito.banking.persistence.model.*
import net.dankito.banking.ui.model.IAccountTransaction
import net.dankito.banking.ui.model.TypedBankAccount
import net.dankito.banking.ui.model.TypedCustomer
import net.dankito.banking.ui.model.tan.MobilePhoneTanMedium
import net.dankito.banking.ui.model.tan.TanGeneratorTanMedium
import net.dankito.banking.util.persistence.doSaveUrlToFile
import net.dankito.utils.multiplatform.File
open class RoomBankingPersistence(applicationContext: Context) : IBankingPersistence {
protected val db = Room.databaseBuilder(
applicationContext,
BankingDatabase::class.java, "banking-database"
).build()
override fun saveOrUpdateAccount(customer: TypedCustomer, allCustomers: List<TypedCustomer>) {
(customer as? Bank)?.let { bank ->
bank.selectedTanProcedureId = bank.selectedTanProcedure?.technicalId
db.bankDao().saveOrUpdate(bank)
// TODO: in this way removed accounts won't be deleted from DB and therefore still be visible to user
val accounts = bank.accounts.filterIsInstance<BankAccount>()
accounts.forEach { it.bankId = bank.id }
db.bankAccountDao().saveOrUpdate(accounts)
// TODO: in this way removed TAN procedures won't be deleted from DB and therefore still be visible to user
val tanProcedures = bank.supportedTanProcedures.filterIsInstance<TanProcedure>()
tanProcedures.forEach { it.bankId = bank.id }
db.tanProcedureDao().saveOrUpdate(tanProcedures)
// TODO: in this way removed TAN procedures won't be deleted from DB and therefore still be visible to user
val tanMedia = bank.tanMedia.map { map(bank, it) }
db.tanMediumDao().saveOrUpdate(tanMedia)
}
}
override fun deleteAccount(customer: TypedCustomer, allCustomers: List<TypedCustomer>) {
(customer as? Bank)?.let { bank ->
db.accountTransactionDao().delete(bank.accounts.flatMap { it.bookedTransactions }.filterIsInstance<AccountTransaction>())
db.bankAccountDao().delete(bank.accounts.filterIsInstance<BankAccount>())
db.tanProcedureDao().delete(bank.supportedTanProcedures.filterIsInstance<TanProcedure>())
db.tanMediumDao().delete(bank.tanMedia.filterIsInstance<TanMedium>())
db.bankDao().delete(bank)
}
}
override fun readPersistedAccounts(): List<TypedCustomer> {
val banks = db.bankDao().getAll()
val accounts = db.bankAccountDao().getAll()
val transactions = db.accountTransactionDao().getAll()
val tanProcedures = db.tanProcedureDao().getAll()
val tanMedia = db.tanMediumDao().getAll()
banks.forEach { bank ->
bank.accounts = accounts.filter { it.bankId == bank.id }
bank.accounts.filterIsInstance<BankAccount>().forEach { account ->
account.customer = bank
account.bookedTransactions = transactions.filter { it.bankAccountId == account.id }
account.bookedTransactions.filterIsInstance<AccountTransaction>().forEach { transaction ->
transaction.bankAccount = account
}
}
bank.supportedTanProcedures = tanProcedures.filter { it.bankId == bank.id }
bank.selectedTanProcedure = bank.supportedTanProcedures.firstOrNull { it.technicalId == bank.selectedTanProcedureId }
bank.tanMedia = tanMedia.filter { it.bankId == bank.id }.map { map(it) }
}
return banks
}
override fun saveOrUpdateAccountTransactions(bankAccount: TypedBankAccount, transactions: List<IAccountTransaction>) {
val accountId = (bankAccount as? BankAccount)?.id ?: bankAccount.technicalId.toLong()
val mappedTransactions = transactions.filterIsInstance<AccountTransaction>()
mappedTransactions.forEach { it.bankAccountId = accountId }
db.accountTransactionDao().saveOrUpdate(mappedTransactions)
}
protected open fun map(bank: Bank, tanMedium: net.dankito.banking.ui.model.tan.TanMedium): TanMedium {
val type = when (tanMedium) {
is TanGeneratorTanMedium -> TanMediumType.TanGeneratorTanMedium
is MobilePhoneTanMedium -> TanMediumType.MobilePhoneTanMedium
else -> TanMediumType.OtherTanMedium
}
return TanMedium(tanMedium.technicalId, bank.id, type, tanMedium.displayName, tanMedium.status,
(tanMedium as? TanGeneratorTanMedium)?.cardNumber, (tanMedium as? MobilePhoneTanMedium)?.phoneNumber)
}
protected open fun map(tanMedium: TanMedium): net.dankito.banking.ui.model.tan.TanMedium {
val displayName = tanMedium.displayName
val status = tanMedium.status
val mapped = when (tanMedium.type) {
TanMediumType.TanGeneratorTanMedium -> TanGeneratorTanMedium(displayName, status, tanMedium.cardNumber ?: "")
TanMediumType.MobilePhoneTanMedium -> MobilePhoneTanMedium(displayName, status, tanMedium.phoneNumber)
else -> net.dankito.banking.ui.model.tan.TanMedium(displayName, status)
}
mapped.technicalId = tanMedium.id
return mapped
}
override fun saveUrlToFile(url: String, file: File) {
doSaveUrlToFile(url, file)
}
}

View File

@ -0,0 +1,91 @@
package net.dankito.banking.persistence
import androidx.room.TypeConverter
import net.dankito.banking.persistence.model.TanMediumType
import net.dankito.banking.ui.model.BankAccountType
import net.dankito.banking.ui.model.tan.AllowedTanFormat
import net.dankito.banking.ui.model.tan.TanMediumStatus
import net.dankito.banking.ui.model.tan.TanProcedureType
import net.dankito.utils.multiplatform.BigDecimal
import net.dankito.utils.multiplatform.Date
open class TypeConverters {
@TypeConverter
fun fromMultiplatformBigDecimal(value: BigDecimal?): String? {
return value?.toPlainString()
}
@TypeConverter
fun toMultiplatformBigDecimal(value: String?): BigDecimal? {
return value?.let { BigDecimal(value) }
}
@TypeConverter
fun fromMultiplatformDate(value: Date?): Long? {
return value?.millisSinceEpoch
}
@TypeConverter
fun toMultiplatformDate(value: Long?): Date? {
return value?.let { Date(value) }
}
@TypeConverter
fun fromBankAccountType(value: BankAccountType): Int {
return value.ordinal
}
@TypeConverter
fun toBankAccountType(value: Int): BankAccountType {
return BankAccountType.values().first { it.ordinal == value }
}
@TypeConverter
fun fromTanProcedureType(value: TanProcedureType): Int {
return value.ordinal
}
@TypeConverter
fun toTanProcedureType(value: Int): TanProcedureType {
return TanProcedureType.values().first { it.ordinal == value }
}
@TypeConverter
fun fromAllowedTanFormat(value: AllowedTanFormat): Int {
return value.ordinal
}
@TypeConverter
fun toAllowedTanFormat(value: Int): AllowedTanFormat {
return AllowedTanFormat.values().first { it.ordinal == value }
}
@TypeConverter
fun fromTanMediumStatus(value: TanMediumStatus): Int {
return value.ordinal
}
@TypeConverter
fun toTanMediumStatus(value: Int): TanMediumStatus {
return TanMediumStatus.values().first { it.ordinal == value }
}
@TypeConverter
fun fromTanMediumTypes(value: TanMediumType): Int {
return value.ordinal
}
@TypeConverter
fun toTanMediumType(value: Int): TanMediumType {
return TanMediumType.values().first { it.ordinal == value }
}
}

View File

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

View File

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

View File

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

View File

@ -0,0 +1,35 @@
package net.dankito.banking.persistence.dao
import androidx.room.*
interface BaseDao<T> {
companion object {
const val ObjectNotInsertedId = -1L
const val IdNotSet = 0L
}
@Insert(onConflict = OnConflictStrategy.IGNORE)
fun insert(obj: T): Long
@Insert(onConflict = OnConflictStrategy.IGNORE)
fun insert(obj: List<T>): List<Long>
@Update(onConflict = OnConflictStrategy.IGNORE)
fun update(obj: T)
@Update(onConflict = OnConflictStrategy.IGNORE)
fun update(obj: List<T>)
@Delete
fun delete(obj: T)
@Delete
fun delete(obj: List<T>)
}

View File

@ -0,0 +1,58 @@
package net.dankito.banking.persistence.dao
import androidx.room.Transaction
import net.dankito.banking.persistence.model.*
/* Room didn't allow me to add these methods to BaseDao directly (Kapt error), so i defined them as extension methods */
@Transaction
fun <T> BaseDao<T>.saveOrUpdate(obj: T) {
val id = insert(obj)
if (wasNotInserted(id)) {
update(obj)
}
else {
setId(obj, id)
}
}
@Transaction
fun <T> BaseDao<T>.saveOrUpdate(objList: List<T>) {
val ids = insert(objList)
// i was not allowed to use mapIndexedNotNull()
val notInsertedObjects = mutableListOf<T>()
ids.forEachIndexed { index, id ->
val obj = objList[index]
if (wasNotInserted(id)) {
notInsertedObjects.add(obj)
}
else {
setId(obj, id)
}
}
update(notInsertedObjects)
}
private fun wasNotInserted(id: Long): Boolean {
return id == BaseDao.ObjectNotInsertedId
}
private fun <T> setId(obj: T, id: Long) {
if (obj is Bank) {
obj.id = id // why doesn't Room set this on itself?
obj.technicalId = obj.id.toString()
}
else if (obj is BankAccount) {
obj.id = id // why doesn't Room set this on itself?
obj.technicalId = obj.id.toString()
}
else if (obj is AccountTransaction) {
obj.id = id // why doesn't Room set this on itself?
obj.technicalId = obj.id.toString()
}
}

View File

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

View File

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

View File

@ -0,0 +1,94 @@
package net.dankito.banking.persistence.model
import androidx.room.Entity
import androidx.room.Ignore
import androidx.room.PrimaryKey
import net.dankito.banking.persistence.dao.BaseDao
import net.dankito.banking.ui.model.IAccountTransaction
import net.dankito.utils.multiplatform.*
@Entity
open class AccountTransaction(
@Ignore
override var bankAccount: BankAccount,
override var amount: BigDecimal,
override var currency: String,
override var unparsedUsage: String,
override var bookingDate: Date,
override var otherPartyName: String?,
override var otherPartyBankCode: String?,
override var otherPartyAccountId: String?,
override var bookingText: String?,
override var valueDate: Date,
override var statementNumber: Int,
override var sequenceNumber: Int?,
override var openingBalance: BigDecimal?,
override var closingBalance: BigDecimal?,
override var endToEndReference: String?,
override var customerReference: String?,
override var mandateReference: String?,
override var creditorIdentifier: String?,
override var originatorsIdentificationCode: String?,
override var compensationAmount: String?,
override var originalAmount: String?,
override var sepaUsage: String?,
override var deviantOriginator: String?,
override var deviantRecipient: String?,
override var usageWithNoSpecialType: String?,
override var primaNotaNumber: String?,
override var textKeySupplement: String?,
override var currencyType: String?,
override var bookingKey: String,
override var referenceForTheAccountOwner: String,
override var referenceOfTheAccountServicingInstitution: String?,
override var supplementaryDetails: String?,
override var transactionReferenceNumber: String,
override var relatedReferenceNumber: String?
) : IAccountTransaction {
// for object deserializers
internal constructor() : this(BankAccount(), null, "", BigDecimal.Zero, Date(), null)
/* convenience constructors for languages not supporting default values */
constructor(bankAccount: BankAccount, otherPartyName: String?, unparsedUsage: String, amount: BigDecimal, valueDate: Date, bookingText: String?)
: this(bankAccount, amount, "EUR", unparsedUsage, valueDate,
otherPartyName, null, null, bookingText, valueDate)
constructor(bankAccount: BankAccount, amount: BigDecimal, currency: String, unparsedUsage: String, bookingDate: Date,
otherPartyName: String?, otherPartyBankCode: String?, otherPartyAccountId: String?,
bookingText: String?, valueDate: Date)
: this(bankAccount, amount, currency, unparsedUsage, bookingDate,
otherPartyName, otherPartyBankCode, otherPartyAccountId, bookingText, valueDate,
0, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, "", "", null, null, "", null)
@PrimaryKey(autoGenerate = true)
open var id: Long = BaseDao.IdNotSet
override var technicalId: String = buildTransactionIdentifier()
// Room doesn't allow me to add getters and setters -> have to map it manually
open var bankAccountId: Long = BaseDao.ObjectNotInsertedId
override fun equals(other: Any?): Boolean {
return doesEqual(other)
}
override fun hashCode(): Int {
return calculateHashCode()
}
override fun toString(): String {
return stringRepresentation
}
}

View File

@ -0,0 +1,55 @@
package net.dankito.banking.persistence.model
import androidx.room.Entity
import androidx.room.Ignore
import androidx.room.PrimaryKey
import net.dankito.banking.persistence.dao.BaseDao
import net.dankito.banking.ui.model.TypedBankAccount
import net.dankito.banking.ui.model.TypedCustomer
import net.dankito.banking.ui.model.tan.TanMedium
import net.dankito.banking.ui.model.tan.TanProcedure
@Entity
open class Bank(
override var bankCode: String,
override var customerId: String,
override var password: String,
override var finTsServerAddress: String,
override var bankName: String,
override var bic: String,
override var customerName: String,
override var userId: String = customerId,
override var iconUrl: String? = null,
@Ignore
override var accounts: List<TypedBankAccount> = listOf(),
@Ignore
override var supportedTanProcedures: List<TanProcedure> = listOf(),
@Ignore
override var selectedTanProcedure: TanProcedure? = null,
@Ignore
override var tanMedia: List<TanMedium> = listOf(),
@PrimaryKey(autoGenerate = true)
open var id: Long = BaseDao.IdNotSet,
override var technicalId: String = id.toString(),
override var userSetDisplayName: String? = null,
override var displayIndex: Int = 0
) : TypedCustomer {
internal constructor() : this("", "", "", "", "", "", "") // for object deserializers
open var selectedTanProcedureId: String? = null
override fun toString(): String {
return stringRepresentation
}
}

View File

@ -0,0 +1,71 @@
package net.dankito.banking.persistence.model
import androidx.room.Entity
import androidx.room.Ignore
import androidx.room.PrimaryKey
import net.dankito.banking.persistence.dao.BaseDao
import net.dankito.banking.ui.model.*
import net.dankito.utils.multiplatform.BigDecimal
import net.dankito.utils.multiplatform.Date
import net.dankito.utils.multiplatform.UUID
@Entity
open class BankAccount(
@Ignore
override var customer: TypedCustomer,
override var identifier: String,
override var accountHolderName: String,
override var iban: String?,
override var subAccountNumber: String?,
override var customerId: String,
override var balance: BigDecimal = BigDecimal.Zero,
override var currency: String = "EUR",
override var type: BankAccountType = BankAccountType.Girokonto,
override var productName: String? = null,
override var accountLimit: String? = null,
override var lastRetrievedTransactionsTimestamp: Date? = null,
override var supportsRetrievingAccountTransactions: Boolean = false,
override var supportsRetrievingBalance: Boolean = false,
override var supportsTransferringMoney: Boolean = false,
override var supportsInstantPaymentMoneyTransfer: Boolean = false,
@Ignore
override var bookedTransactions: List<IAccountTransaction> = listOf(),
@Ignore
override var unbookedTransactions: List<Any> = listOf()
) : TypedBankAccount {
internal constructor() : this(Bank(), null, "") // for object deserializers
/* convenience constructors for languages not supporting default values */
constructor(customer: TypedCustomer, productName: String?, identifier: String) : this(customer, productName, identifier, BankAccountType.Girokonto)
constructor(customer: TypedCustomer, productName: String?, identifier: String, type: BankAccountType = BankAccountType.Girokonto, balance: BigDecimal = BigDecimal.Zero)
: this(customer, identifier, "", null, null, "", balance, "EUR", type, productName)
@PrimaryKey(autoGenerate = true)
open var id: Long = BaseDao.IdNotSet
override var technicalId: String = UUID.random()
// Room doesn't allow me to add getters and setters -> have to map it manually
open var bankId: Long = BaseDao.ObjectNotInsertedId
override var haveAllTransactionsBeenFetched: Boolean = false
override var userSetDisplayName: String? = null
override var displayIndex: Int = 0
override fun toString(): String {
return stringRepresentation
}
}

View File

@ -0,0 +1,73 @@
package net.dankito.banking.persistence.model
import net.dankito.banking.ui.model.IAccountTransaction
import net.dankito.banking.ui.model.TypedBankAccount
import net.dankito.banking.ui.model.TypedCustomer
import net.dankito.banking.ui.model.mapper.IModelCreator
import net.dankito.banking.ui.model.tan.AllowedTanFormat
import net.dankito.banking.ui.model.tan.TanProcedureType
import net.dankito.utils.multiplatform.BigDecimal
import net.dankito.utils.multiplatform.Date
open class RoomModelCreator : IModelCreator {
override fun createCustomer(bankCode: String, customerId: String, password: String, finTsServerAddress: String, bankName: String,
bic: String, customerName: String, userId: String, iconUrl: String?): TypedCustomer {
return Bank(bankCode, customerId, password, finTsServerAddress, bankName, bic, customerName, userId, iconUrl)
}
override fun createBankAccount(customer: TypedCustomer, productName: String?, identifier: String): TypedBankAccount {
return BankAccount(customer, productName, identifier)
}
override fun createTransaction(
bankAccount: TypedBankAccount,
amount: BigDecimal,
currency: String,
unparsedUsage: String,
bookingDate: Date,
otherPartyName: String?,
otherPartyBankCode: String?,
otherPartyAccountId: String?,
bookingText: String?,
valueDate: Date,
statementNumber: Int,
sequenceNumber: Int?,
openingBalance: BigDecimal?,
closingBalance: BigDecimal?,
endToEndReference: String?,
customerReference: String?,
mandateReference: String?,
creditorIdentifier: String?,
originatorsIdentificationCode: String?,
compensationAmount: String?,
originalAmount: String?,
sepaUsage: String?,
deviantOriginator: String?,
deviantRecipient: String?,
usageWithNoSpecialType: String?,
primaNotaNumber: String?,
textKeySupplement: String?,
currencyType: String?,
bookingKey: String,
referenceForTheAccountOwner: String,
referenceOfTheAccountServicingInstitution: String?,
supplementaryDetails: String?,
transactionReferenceNumber: String,
relatedReferenceNumber: String?
): IAccountTransaction {
return AccountTransaction(bankAccount as BankAccount, amount, currency, unparsedUsage, bookingDate, otherPartyName, otherPartyBankCode, otherPartyAccountId,
bookingText, valueDate, statementNumber, sequenceNumber, openingBalance, closingBalance, endToEndReference, customerReference, mandateReference,
creditorIdentifier, originatorsIdentificationCode, compensationAmount, originalAmount, sepaUsage, deviantOriginator, deviantRecipient,
usageWithNoSpecialType, primaNotaNumber, textKeySupplement, currencyType, bookingKey, referenceForTheAccountOwner,
referenceOfTheAccountServicingInstitution, supplementaryDetails, transactionReferenceNumber, relatedReferenceNumber)
}
override fun createTanProcedure(displayName: String, type: TanProcedureType, bankInternalProcedureCode: String, maxTanInputLength: Int?, allowedTanFormat: AllowedTanFormat): net.dankito.banking.ui.model.tan.TanProcedure {
return TanProcedure(displayName, type, bankInternalProcedureCode, maxTanInputLength, allowedTanFormat)
}
}

View File

@ -0,0 +1,24 @@
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.tan.TanMediumStatus
@Entity
open class TanMedium(
@PrimaryKey
open var id: String,
open var bankId: Long,
open var type: TanMediumType,
open var displayName: String,
open var status: TanMediumStatus,
open var cardNumber: String? = null,
open var phoneNumber: String? = null
) {
internal constructor() : this("", BaseDao.ObjectNotInsertedId, TanMediumType.OtherTanMedium, "", TanMediumStatus.Available) // for object deserializers
}

View File

@ -0,0 +1,12 @@
package net.dankito.banking.persistence.model
enum class TanMediumType {
TanGeneratorTanMedium,
MobilePhoneTanMedium,
OtherTanMedium
}

View File

@ -0,0 +1,29 @@
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.tan.AllowedTanFormat
import net.dankito.banking.ui.model.tan.TanProcedure
import net.dankito.banking.ui.model.tan.TanProcedureType
@Entity
open class TanProcedure(
displayName: String,
type: TanProcedureType,
bankInternalProcedureCode: String,
maxTanInputLength: Int? = null,
allowedTanFormat: AllowedTanFormat = AllowedTanFormat.Alphanumeric
) : TanProcedure(displayName, type, bankInternalProcedureCode, maxTanInputLength, allowedTanFormat) {
internal constructor() : this("", TanProcedureType.EnterTan, "") // for object deserializers
@PrimaryKey
open var id: String = technicalId
// Room doesn't allow me to add getters and setters -> have to map it manually
open var bankId: Long = BaseDao.ObjectNotInsertedId
}

View File

@ -4,8 +4,7 @@ import net.dankito.banking.persistence.model.CustomerEntity
import net.dankito.banking.ui.model.*
import net.dankito.utils.multiplatform.File
import net.dankito.banking.util.ISerializer
import java.io.FileOutputStream
import java.net.URL
import net.dankito.banking.util.persistence.doSaveUrlToFile
open class BankingPersistenceJson(
@ -43,11 +42,7 @@ open class BankingPersistenceJson(
override fun saveUrlToFile(url: String, file: File) {
URL(url).openConnection().getInputStream().buffered().use { iconInputStream ->
FileOutputStream(file).use { fileOutputStream ->
iconInputStream.copyTo(fileOutputStream)
}
}
doSaveUrlToFile(url, file)
}
}

View File

@ -70,4 +70,8 @@ open class AccountTransactionEntity(
return calculateHashCode()
}
override fun toString(): String {
return stringRepresentation
}
}

View File

@ -38,4 +38,9 @@ open class BankAccountEntity(
internal constructor() : this(CustomerEntity(), "", "", null, null, "") // for object deserializers
override fun toString(): String {
return stringRepresentation
}
}

View File

@ -30,4 +30,9 @@ open class CustomerEntity(
internal constructor() : this("", "", "", "", "", "", "") // for object deserializers
override fun toString(): String {
return stringRepresentation
}
}

View File

@ -18,6 +18,7 @@ include ':hbci4jBankingClient'
include ':BankingPersistenceJson'
include ':LuceneBankingPersistence'
include ':RoomBankingPersistence'
include ':BankingAndroidApp'
@ -41,6 +42,7 @@ project(':BankingUiNativeIntegration').projectDir = "$rootDir/ui/BankingUiNative
project(':BankingPersistenceJson').projectDir = "$rootDir/persistence/json/BankingPersistenceJson/" as File
project(':LuceneBankingPersistence').projectDir = "$rootDir/persistence/LuceneBankingPersistence/" as File
project(':RoomBankingPersistence').projectDir = "$rootDir/persistence/database/RoomBankingPersistence/" as File

View File

@ -91,6 +91,7 @@ dependencies {
implementation project(':BankingPersistenceJson')
implementation project(':LuceneBankingPersistence')
implementation project(':RoomBankingPersistence')
implementation "net.dankito.text.extraction:itext2-text-extractor:$textExtractorVersion"

View File

@ -9,7 +9,6 @@ import net.dankito.banking.ui.android.RouterAndroid
import net.dankito.banking.ui.android.util.CurrentActivityTracker
import net.dankito.banking.fints4kBankingClientCreator
import net.dankito.banking.persistence.IBankingPersistence
import net.dankito.banking.persistence.LuceneBankingPersistence
import net.dankito.banking.search.IRemitteeSearcher
import net.dankito.banking.search.LuceneRemitteeSearcher
import net.dankito.banking.ui.IBankingClientCreator
@ -17,7 +16,8 @@ import net.dankito.banking.ui.IRouter
import net.dankito.banking.ui.presenter.BankingPresenter
import net.dankito.banking.bankfinder.IBankFinder
import net.dankito.banking.bankfinder.LuceneBankFinder
import net.dankito.banking.persistence.mapper.EntitiesModelCreator
import net.dankito.banking.persistence.RoomBankingPersistence
import net.dankito.banking.persistence.model.RoomModelCreator
import net.dankito.banking.ui.model.mapper.IModelCreator
import net.dankito.utils.multiplatform.toFile
import net.dankito.banking.util.*
@ -115,8 +115,8 @@ class BankingModule(private val applicationContext: Context) {
@Provides
@Singleton
fun provideBankingPersistence(@Named(IndexFolderKey) indexFolder: File, @Named(DatabaseFolderKey) databaseFolder: File, serializer: ISerializer) : IBankingPersistence {
return LuceneBankingPersistence(indexFolder, databaseFolder, serializer)
fun provideBankingPersistence() : IBankingPersistence {
return RoomBankingPersistence(applicationContext)
}
@Provides
@ -169,7 +169,7 @@ class BankingModule(private val applicationContext: Context) {
@Provides
@Singleton
fun provideModelCreator() : IModelCreator {
return EntitiesModelCreator()
return RoomModelCreator()
}
@Provides

View File

@ -77,7 +77,7 @@ open class AccountTransaction(
override fun toString(): String {
return "${DateFormatter(DateFormatStyle.Medium).format(valueDate)} $amount $otherPartyName: $usage"
return stringRepresentation
}
}

View File

@ -49,7 +49,7 @@ open class BankAccount @JvmOverloads constructor(
override fun toString(): String {
return "$accountHolderName ($identifier)"
return stringRepresentation
}
}

View File

@ -43,7 +43,7 @@ open class Customer(
override fun toString(): String {
return "$bankName $customerId"
return stringRepresentation
}
}

View File

@ -2,6 +2,7 @@ package net.dankito.banking.ui.model
import net.dankito.utils.multiplatform.BigDecimal
import net.dankito.utils.multiplatform.Date
import net.dankito.utils.multiplatform.DateFormatStyle
import net.dankito.utils.multiplatform.DateFormatter
@ -106,5 +107,8 @@ interface IAccountTransaction {
result = 31 * result + valueDate.hashCode()
return result
}
val stringRepresentation: String
get() = "${DateFormatter(DateFormatStyle.Medium).format(valueDate)} $amount $otherPartyName: $usage"
}

View File

@ -52,5 +52,9 @@ interface IBankAccount<TTransaction: IAccountTransaction> : OrderedDisplayable {
this.unbookedTransactions = uniqueUnbookedTransactions.toList()
}
val stringRepresentation: String
get() = "$accountHolderName ($identifier)"
}

View File

@ -53,4 +53,8 @@ interface ICustomer<TAccount: IBankAccount<TAccountTransaction>, TAccountTransac
val tanMediaSorted: List<TanMedium>
get() = tanMedia.sortedByDescending { it.status == TanMediumStatus.Used }
val stringRepresentation: String
get() = "$bankName $customerId"
}

View File

@ -1,6 +1,7 @@
package net.dankito.banking.ui.model.mapper
import net.dankito.banking.ui.model.*
import net.dankito.banking.ui.model.tan.*
import net.dankito.utils.multiplatform.BigDecimal
import net.dankito.utils.multiplatform.Date
@ -54,4 +55,22 @@ interface IModelCreator {
relatedReferenceNumber: String?
) : IAccountTransaction
fun createTanProcedure(displayName: String, type: TanProcedureType, bankInternalProcedureCode: String,
maxTanInputLength: Int? = null, allowedTanFormat: AllowedTanFormat = AllowedTanFormat.Alphanumeric): TanProcedure {
return TanProcedure(displayName, type, bankInternalProcedureCode, maxTanInputLength)
}
fun createTanMedium(displayName: String, status: TanMediumStatus): TanMedium {
return TanMedium(displayName, status)
}
fun createTanGeneratorTanMedium(displayName: String, status: TanMediumStatus, cardNumber: String): TanGeneratorTanMedium {
return TanGeneratorTanMedium(displayName, status, cardNumber)
}
fun createMobilePhoneTanMedium(displayName: String, status: TanMediumStatus, phoneNumber: String?): MobilePhoneTanMedium {
return MobilePhoneTanMedium(displayName, status, phoneNumber)
}
}

View File

@ -6,7 +6,7 @@ import net.dankito.utils.multiplatform.UUID
open class TanMedium(
override val displayName: String,
val status: TanMediumStatus
open val status: TanMediumStatus
) : Displayable {

View File

@ -2,21 +2,23 @@ package net.dankito.banking.ui.model.tan
import net.dankito.banking.ui.model.Displayable
import net.dankito.utils.multiplatform.UUID
import kotlin.jvm.Transient
open class TanProcedure(
override val displayName: String,
val type: TanProcedureType,
val bankInternalProcedureCode: String,
val maxTanInputLength: Int? = null,
val allowedTanFormat: AllowedTanFormat = AllowedTanFormat.Alphanumeric
open val type: TanProcedureType,
open val bankInternalProcedureCode: String,
open val maxTanInputLength: Int? = null,
open val allowedTanFormat: AllowedTanFormat = AllowedTanFormat.Alphanumeric
) : Displayable {
internal constructor() : this("", TanProcedureType.EnterTan, "") // for object deserializers
val isNumericTan: Boolean = allowedTanFormat == AllowedTanFormat.Numeric
@Transient
open val isNumericTan: Boolean = allowedTanFormat == AllowedTanFormat.Numeric
open var technicalId: String = UUID.random()

View File

@ -0,0 +1,15 @@
package net.dankito.banking.util.persistence
import net.dankito.banking.persistence.IBankingPersistence
import net.dankito.utils.multiplatform.File
import java.io.FileOutputStream
import java.net.URL
fun IBankingPersistence.doSaveUrlToFile(url: String, file: File) {
URL(url).openConnection().getInputStream().buffered().use { iconInputStream ->
FileOutputStream(file).use { fileOutputStream ->
iconInputStream.copyTo(fileOutputStream)
}
}
}

View File

@ -253,7 +253,7 @@ open class fints4kModelMapper(protected val modelCreator: IModelCreator) {
account.selectedTanProcedure = null
}
account.tanMedia = mapTanMediums(bank.tanMedia)
account.tanMedia = mapTanMedia(bank.tanMedia)
}
@ -262,7 +262,7 @@ open class fints4kModelMapper(protected val modelCreator: IModelCreator) {
}
open fun mapTanProcedure(tanProcedure: net.dankito.banking.fints.model.TanProcedure): TanProcedure {
return TanProcedure(
return modelCreator.createTanProcedure(
tanProcedure.displayName,
mapTanProcedureType(tanProcedure.type),
tanProcedure.securityFunction.code,
@ -306,40 +306,34 @@ open class fints4kModelMapper(protected val modelCreator: IModelCreator) {
}
open fun mapTanMediums(tanMediums: List<net.dankito.banking.fints.messages.datenelemente.implementierte.tan.TanMedium>): List<TanMedium> {
open fun mapTanMedia(tanMediums: List<net.dankito.banking.fints.messages.datenelemente.implementierte.tan.TanMedium>): List<TanMedium> {
return tanMediums.map { mapTanMedium(it) }
}
open fun mapTanMedium(tanMedium: net.dankito.banking.fints.messages.datenelemente.implementierte.tan.TanMedium): TanMedium {
val displayName = getDisplayNameForTanMedium(tanMedium)
val status = mapTanMediumStatus(tanMedium)
// TODO: irgendwas ging hier schief
if (tanMedium is net.dankito.banking.fints.messages.datenelemente.implementierte.tan.TanGeneratorTanMedium) {
return mapTanMedium(tanMedium)
return mapTanMedium(tanMedium, displayName, status)
}
if (tanMedium is net.dankito.banking.fints.messages.datenelemente.implementierte.tan.MobilePhoneTanMedium) {
return mapTanMedium(tanMedium)
return modelCreator.createMobilePhoneTanMedium(displayName, status, tanMedium.phoneNumber ?: tanMedium.concealedPhoneNumber)
}
return TanMedium(
getDisplayNameForTanMedium(tanMedium),
mapTanMediumStatus(tanMedium)
)
return modelCreator.createTanMedium(displayName, status)
}
open fun mapTanMedium(tanMedium: net.dankito.banking.fints.messages.datenelemente.implementierte.tan.TanGeneratorTanMedium): TanGeneratorTanMedium {
return TanGeneratorTanMedium(
getDisplayNameForTanMedium(tanMedium),
mapTanMediumStatus(tanMedium),
tanMedium.cardNumber
)
return mapTanMedium(tanMedium, getDisplayNameForTanMedium(tanMedium), mapTanMediumStatus(tanMedium))
}
open fun mapTanMedium(tanMedium: net.dankito.banking.fints.messages.datenelemente.implementierte.tan.MobilePhoneTanMedium): MobilePhoneTanMedium {
return MobilePhoneTanMedium(
getDisplayNameForTanMedium(tanMedium),
mapTanMediumStatus(tanMedium),
tanMedium.phoneNumber ?: tanMedium.concealedPhoneNumber
)
protected open fun mapTanMedium(tanMedium: net.dankito.banking.fints.messages.datenelemente.implementierte.tan.TanGeneratorTanMedium,
displayName: String, status: TanMediumStatus): TanGeneratorTanMedium {
return modelCreator.createTanGeneratorTanMedium(displayName, status, tanMedium.cardNumber)
}
protected open fun getDisplayNameForTanMedium(tanMedium: net.dankito.banking.fints.messages.datenelemente.implementierte.tan.TanMedium): String {

View File

@ -106,15 +106,15 @@ open class hbci4jModelMapper(
// TODO: implement all TAN procedures
displayNameLowerCase.contains("chiptan") -> {
if (displayNameLowerCase.contains("qr")) {
net.dankito.banking.ui.model.tan.TanProcedure(displayName, TanProcedureType.ChipTanQrCode, code)
modelCreator.createTanProcedure(displayName, TanProcedureType.ChipTanQrCode, code)
}
else {
net.dankito.banking.ui.model.tan.TanProcedure(displayName, TanProcedureType.ChipTanFlickercode, code)
modelCreator.createTanProcedure(displayName, TanProcedureType.ChipTanFlickercode, code)
}
}
displayNameLowerCase.contains("sms") -> net.dankito.banking.ui.model.tan.TanProcedure(displayName, TanProcedureType.SmsTan, code)
displayNameLowerCase.contains("push") -> net.dankito.banking.ui.model.tan.TanProcedure(displayName, TanProcedureType.AppTan, code)
displayNameLowerCase.contains("sms") -> modelCreator.createTanProcedure(displayName, TanProcedureType.SmsTan, code)
displayNameLowerCase.contains("push") -> modelCreator.createTanProcedure(displayName, TanProcedureType.AppTan, code)
// we filter out iTAN and Einschritt-Verfahren as they are not permitted anymore according to PSD2
else -> null