diff --git a/build.gradle b/build.gradle index dbef3af4..c41cc145 100644 --- a/build.gradle +++ b/build.gradle @@ -23,6 +23,8 @@ ext { textInfoExtractorVersion = "1.0.1" + commonsCsvVersion = "1.8" + hbci4jVersion = '3.1.37' @@ -45,7 +47,7 @@ ext { androidTargetSdkVersion = 28 - fileChooserDialogVersion = "1.2.0-androidx" + fileChooserDialogVersion = "1.3.0-androidx" androidUtilsVersion = '1.1.1-SNAPSHOT' diff --git a/persistence/database/RoomBankingPersistence/src/main/java/net/dankito/banking/persistence/RoomBankingPersistence.kt b/persistence/database/RoomBankingPersistence/src/main/java/net/dankito/banking/persistence/RoomBankingPersistence.kt index aca6ca5f..2aad2925 100644 --- a/persistence/database/RoomBankingPersistence/src/main/java/net/dankito/banking/persistence/RoomBankingPersistence.kt +++ b/persistence/database/RoomBankingPersistence/src/main/java/net/dankito/banking/persistence/RoomBankingPersistence.kt @@ -190,7 +190,7 @@ open class RoomBankingPersistence(protected open val applicationContext: Context override fun saveOrUpdateAppSettings(appSettings: AppSettings) { val mapped = net.dankito.banking.persistence.model.AppSettings(appSettings.automaticallyUpdateAccountsAfterMinutes, - appSettings.lockAppAfterMinutes, appSettings.screenshotsAllowed) + appSettings.lockAppAfterMinutes, appSettings.screenshotsAllowed, appSettings.lastSelectedExportFolder) database.appSettingsDao().saveOrUpdate(mapped) saveOrUpdateTanMethodSettings(appSettings.flickerCodeSettings, FlickerCodeTanMethodSettingsId) @@ -215,6 +215,7 @@ open class RoomBankingPersistence(protected open val applicationContext: Context settings.automaticallyUpdateAccountsAfterMinutes = persistedSettings.automaticallyUpdateAccountsAfterMinutes settings.lockAppAfterMinutes = persistedSettings.lockAppAfterMinutes settings.screenshotsAllowed = persistedSettings.screenshotsAllowed + settings.lastSelectedExportFolder = persistedSettings.lastSelectedExportFolder } settings.flickerCodeSettings = findTanMethodSettings(FlickerCodeTanMethodSettingsId, tanMethodSettings) diff --git a/persistence/database/RoomBankingPersistence/src/main/java/net/dankito/banking/persistence/model/AppSettings.kt b/persistence/database/RoomBankingPersistence/src/main/java/net/dankito/banking/persistence/model/AppSettings.kt index 2ac1388f..ebc8e81a 100644 --- a/persistence/database/RoomBankingPersistence/src/main/java/net/dankito/banking/persistence/model/AppSettings.kt +++ b/persistence/database/RoomBankingPersistence/src/main/java/net/dankito/banking/persistence/model/AppSettings.kt @@ -10,7 +10,8 @@ import net.dankito.banking.ui.model.settings.AppSettings open class AppSettings( open var automaticallyUpdateAccountsAfterMinutes: Int? = AppSettings.DefaultAutomaticallyUpdateAccountsAfterMinutes, open var lockAppAfterMinutes: Int? = null, - open var screenshotsAllowed: Boolean = false + open var screenshotsAllowed: Boolean = false, + open var lastSelectedExportFolder: String? = null ) { internal constructor() : this(AppSettings.DefaultAutomaticallyUpdateAccountsAfterMinutes, null, false) diff --git a/settings.gradle b/settings.gradle index b5b4098e..540066df 100644 --- a/settings.gradle +++ b/settings.gradle @@ -71,10 +71,12 @@ project(':fints4kRest').projectDir = "$rootDir/rest/fints4kRest/" as File include ':BankFinder' include ':LuceneBankFinder' include ':BankListCreator' +include ':CsvAccountTransactionsImporterAndExporter' include ':EpcQrCodeParser' project(':BankFinder').projectDir = "$rootDir/tools/BankFinder/" as File project(':LuceneBankFinder').projectDir = "$rootDir/tools/LuceneBankFinder/" as File project(':BankListCreator').projectDir = "$rootDir/tools/BankListCreator/" as File +project(':CsvAccountTransactionsImporterAndExporter').projectDir = "$rootDir/tools/CsvAccountTransactionsImporterAndExporter/" as File project(':EpcQrCodeParser').projectDir = "$rootDir/tools/EpcQrCodeParser/" as File diff --git a/tools/CsvAccountTransactionsImporterAndExporter/build.gradle b/tools/CsvAccountTransactionsImporterAndExporter/build.gradle new file mode 100644 index 00000000..b96581a8 --- /dev/null +++ b/tools/CsvAccountTransactionsImporterAndExporter/build.gradle @@ -0,0 +1,24 @@ +plugins { + id 'org.jetbrains.kotlin.jvm' + id "maven-publish" +} + +group 'net.codinux.banking.tools' + +ext.artifactName = "csv-account-transactions-importer-exporter" + + +dependencies { + implementation "org.jetbrains.kotlin:kotlin-stdlib" + + implementation "org.apache.commons:commons-csv:$commonsCsvVersion" + + implementation "org.slf4j:slf4j-api:$slf4jVersion" + + + testImplementation "org.junit.jupiter:junit-jupiter:$junit5Version" + testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:$junit5Version" + + testImplementation "org.assertj:assertj-core:$assertJVersion" + testImplementation "org.mockito:mockito-core:$mockitoVersion" +} diff --git a/tools/CsvAccountTransactionsImporterAndExporter/src/main/kotlin/net/codinux/banking/tools/importerexporter/CsvAccountTransactionsExporter.kt b/tools/CsvAccountTransactionsImporterAndExporter/src/main/kotlin/net/codinux/banking/tools/importerexporter/CsvAccountTransactionsExporter.kt new file mode 100644 index 00000000..6a5ed693 --- /dev/null +++ b/tools/CsvAccountTransactionsImporterAndExporter/src/main/kotlin/net/codinux/banking/tools/importerexporter/CsvAccountTransactionsExporter.kt @@ -0,0 +1,83 @@ +package net.codinux.banking.tools.importerexporter + +import net.codinux.banking.tools.importerexporter.model.AccountTransaction +import org.apache.commons.csv.CSVFormat +import org.apache.commons.csv.CSVPrinter +import org.slf4j.LoggerFactory +import java.io.Writer +import java.math.BigDecimal +import java.text.DateFormat +import java.text.NumberFormat +import java.util.* + + +open class CsvAccountTransactionsExporter : IAccountTransactionsExporter { + + companion object { + + // TODO: translate + val Headers = listOf( + "Auftragskonto", "Buchungstag", "Wertstellungstag", "Umsatzart", "Empfänger / Auftraggeber", + "Verwendungszweck", "IBAN/Kontonummer", "BIC", "Umsatz", "Währung" + ) + + const val DateSeparators = "[./ -]" + val DateOnlyContainsNumbersRegex = Regex("\\d{1,4}$DateSeparators\\d{1,2}$DateSeparators\\d{2,4}") + + private val log = LoggerFactory.getLogger(CsvAccountTransactionsExporter::class.java) + } + + + // set as fields not as companion object members so they use the Locale set at CsvAccountTransactionsExporter instantiation time not when the first CsvAccountTransactionsExporter has been created + + protected open val DateFormatter: DateFormat = findLongestDateFormatWithoutWrittenOutMonth() // ensure converted dates only contain numbers, not dates like 27 Mar 2020 + + protected open val DecimalFormat = NumberFormat.getNumberInstance() + + + override fun export(writer: Writer, transactions: Collection) { + try { + writer.use { + val csvPrinter = CSVPrinter(writer, CSVFormat.DEFAULT.withHeader(*Headers.toTypedArray())) + + transactions.forEach { transaction -> + csvPrinter.printRecord(transaction.account, format(transaction.bookingDate), format(transaction.valueDate), + format(transaction.bookingText), format(transaction.otherPartyName), transaction.reference, + format(transaction.otherPartyAccountId), format(transaction.otherPartyBankCode), format(transaction.amount), transaction.currency + ) + } + + csvPrinter.flush() + } + } catch (e: Exception) { + log.error("Could not export ${transactions.size} transactions to CSV", e) + } + } + + + protected open fun format(date: Date): String { + return DateFormatter.format(date) + } + + protected open fun format(bigDecimal: BigDecimal): String { + return DecimalFormat.format(bigDecimal) + } + + protected open fun format(string: String?): String { + return string ?: "" + } + + + protected open fun findLongestDateFormatWithoutWrittenOutMonth(): DateFormat { + val mediumDateFormat = DateFormat.getDateInstance(DateFormat.MEDIUM) + val dateFormatTest = mediumDateFormat.format(Date()) + + return if (DateOnlyContainsNumbersRegex.matches(dateFormatTest)) { + mediumDateFormat + } + else { + DateFormat.getDateInstance(DateFormat.SHORT) + } + } + +} \ No newline at end of file diff --git a/tools/CsvAccountTransactionsImporterAndExporter/src/main/kotlin/net/codinux/banking/tools/importerexporter/IAccountTransactionsExporter.kt b/tools/CsvAccountTransactionsImporterAndExporter/src/main/kotlin/net/codinux/banking/tools/importerexporter/IAccountTransactionsExporter.kt new file mode 100644 index 00000000..470e7892 --- /dev/null +++ b/tools/CsvAccountTransactionsImporterAndExporter/src/main/kotlin/net/codinux/banking/tools/importerexporter/IAccountTransactionsExporter.kt @@ -0,0 +1,16 @@ +package net.codinux.banking.tools.importerexporter + +import net.codinux.banking.tools.importerexporter.model.AccountTransaction +import java.io.File +import java.io.Writer + + +interface IAccountTransactionsExporter { + + fun export(file: File, transactions: Collection) { + return export(file.outputStream().bufferedWriter(), transactions) + } + + fun export(writer: Writer, transactions: Collection) + +} \ No newline at end of file diff --git a/tools/CsvAccountTransactionsImporterAndExporter/src/main/kotlin/net/codinux/banking/tools/importerexporter/IAccountTransactionsImporter.kt b/tools/CsvAccountTransactionsImporterAndExporter/src/main/kotlin/net/codinux/banking/tools/importerexporter/IAccountTransactionsImporter.kt new file mode 100644 index 00000000..7aeddec7 --- /dev/null +++ b/tools/CsvAccountTransactionsImporterAndExporter/src/main/kotlin/net/codinux/banking/tools/importerexporter/IAccountTransactionsImporter.kt @@ -0,0 +1,11 @@ +package net.codinux.banking.tools.importerexporter + +import net.codinux.banking.tools.importerexporter.model.AccountTransaction +import java.io.File + + +interface IAccountTransactionsImporter { + + fun import(file: File): List + +} \ No newline at end of file diff --git a/tools/CsvAccountTransactionsImporterAndExporter/src/main/kotlin/net/codinux/banking/tools/importerexporter/IAccountTransactionsImporterExporter.kt b/tools/CsvAccountTransactionsImporterAndExporter/src/main/kotlin/net/codinux/banking/tools/importerexporter/IAccountTransactionsImporterExporter.kt new file mode 100644 index 00000000..d908ec34 --- /dev/null +++ b/tools/CsvAccountTransactionsImporterAndExporter/src/main/kotlin/net/codinux/banking/tools/importerexporter/IAccountTransactionsImporterExporter.kt @@ -0,0 +1,4 @@ +package net.codinux.banking.tools.importerexporter + + +interface IAccountTransactionsImporterExporter : IAccountTransactionsImporter, IAccountTransactionsExporter \ No newline at end of file diff --git a/tools/CsvAccountTransactionsImporterAndExporter/src/main/kotlin/net/codinux/banking/tools/importerexporter/model/AccountTransaction.kt b/tools/CsvAccountTransactionsImporterAndExporter/src/main/kotlin/net/codinux/banking/tools/importerexporter/model/AccountTransaction.kt new file mode 100644 index 00000000..bf491d3b --- /dev/null +++ b/tools/CsvAccountTransactionsImporterAndExporter/src/main/kotlin/net/codinux/banking/tools/importerexporter/model/AccountTransaction.kt @@ -0,0 +1,20 @@ +package net.codinux.banking.tools.importerexporter.model + +import java.math.BigDecimal +import java.util.* + + +open class AccountTransaction( + open val account: String, + open val amount: BigDecimal, + open val currency: String, + open val reference: String, + open val bookingDate: Date, + open val valueDate: Date, + open val otherPartyName: String?, + open val otherPartyBankCode: String?, + open val otherPartyAccountId: String?, + open val bookingText: String? +) { + +} \ No newline at end of file diff --git a/tools/CsvAccountTransactionsImporterAndExporter/src/test/kotlin/net/codinux/banking/tools/importerexporter/CsvAccountTransactionsExporterTest.kt b/tools/CsvAccountTransactionsImporterAndExporter/src/test/kotlin/net/codinux/banking/tools/importerexporter/CsvAccountTransactionsExporterTest.kt new file mode 100644 index 00000000..3221f316 --- /dev/null +++ b/tools/CsvAccountTransactionsImporterAndExporter/src/test/kotlin/net/codinux/banking/tools/importerexporter/CsvAccountTransactionsExporterTest.kt @@ -0,0 +1,172 @@ +package net.codinux.banking.tools.importerexporter + +import net.codinux.banking.tools.importerexporter.model.AccountTransaction +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.Test + +import org.junit.jupiter.api.Assertions.* +import java.awt.print.Book +import java.io.StringWriter +import java.math.BigDecimal +import java.util.* +import java.util.concurrent.ThreadLocalRandom + +internal class CsvAccountTransactionsExporterTest { + + companion object { + const val AccountId = "DE00876543210123456789" + + const val Currency = "EUR" + + val BookingDateDay = 26 // one day before value date + val ValueDateDay = 27 + val DateMonth = 3 + val DateYear = 1988 + + val BookingDate = Date(DateYear - 1900, DateMonth - 1, BookingDateDay) + + val ValueDate = Date(DateYear - 1900, DateMonth - 1, ValueDateDay) + + const val ReferenceWithUmlaute = "Was für ein schöner Verwendungszweck!" + + const val AsciiReference = "A total normal reference" + + const val Amount1String = "84.23" + val Amount1 = BigDecimal(Amount1String) + + const val OtherParty1Name = "Nelson Mandela" + const val OtherParty1BankCode = "ABCDEFGH123" + const val OtherParty1AccountId = "SA99012345679876543210" + + const val BookingText1 = "Überweisung" + + const val Amount2String = "-123.45" + val Amount2 = BigDecimal(Amount2String) + + val OtherParty2Name: String? = null + val OtherParty2BankCode: String? = null + val OtherParty2AccountId : String? = null + + const val BookingText2 = "Bargeldabhebung" + } + + + private val defaultLocale = Locale.getDefault(Locale.Category.FORMAT) + + + @AfterEach + fun tearDown() { + setLocale(defaultLocale) // restore locale + } + + + @Test + fun exportWithEnglishLocale() { + setLocale(Locale.US) + + val underTest = CsvAccountTransactionsExporter() // has to be created after locale is set as otherwise DateFormat works with the wrong locale + + val transactions = createTransactions(2) + + val writer = StringWriter() + + underTest.export(writer, transactions) + + + val result = writer.toString() + + assertThat(result).contains(Amount1String) + assertThat(result).contains(Amount2String) + + assertThat(result).contains("$DateMonth/$BookingDateDay/${DateYear - 1900}") + assertThat(result).contains("$DateMonth/$ValueDateDay/${DateYear - 1900}") + + assertThat(result).contains(ReferenceWithUmlaute, AsciiReference) + +// assertThat(countOccurrences(result, ',')).isEqualTo(calculateCountSeparators(2)) // actually first thought using the German standard delimiter ';', but now sticking with ',' + } + + @Test + fun exportWithGermanLocale() { + setLocale(Locale.GERMANY) + + val underTest = CsvAccountTransactionsExporter() // has to be created after locale is set as otherwise DateFormat works with the wrong locale + + val transactions = createTransactions(2) + + val writer = StringWriter() + + underTest.export(writer, transactions) + + + val result = writer.toString() + + assertThat(result).contains(Amount1String.replace('.', ',')) + assertThat(result).contains(Amount2String.replace('.', ',')) + + assertThat(result).contains("$BookingDateDay.0$DateMonth.$DateYear") + assertThat(result).contains("$ValueDateDay.0$DateMonth.$DateYear") + + assertThat(result).contains(ReferenceWithUmlaute, AsciiReference) + +// assertThat(countOccurrences(result, ',')).isEqualTo(calculateCountSeparators(2)) // actually first thought using the German standard delimiter ';', but now sticking with ',' + } + + + private fun createTransactions(count: Int): List { + val transactions = mutableListOf() + + IntRange(0, count - 1).forEach { index -> + transactions.add(createTransaction(index)) + } + + return transactions + } + + private fun createTransaction(index: Int): AccountTransaction { + return when (index) { + 0 -> createTransaction(Amount1, ReferenceWithUmlaute, OtherParty1Name, OtherParty1BankCode, OtherParty1AccountId, BookingText1) + 1 -> createTransaction(Amount2, AsciiReference, OtherParty2Name, OtherParty2BankCode, OtherParty2AccountId, BookingText2) + else -> createRandomTransaction() + } + } + + private fun createRandomTransaction(): AccountTransaction { + val random = ThreadLocalRandom.current() + + val amount = random.nextDouble(-1_000_000.0, 1_000_000.0) + + return createTransaction(BigDecimal.valueOf(amount), "") + } + + private fun createTransaction(amount: BigDecimal, reference: String, otherPartyName: String? = null, otherPartyBankCode: String? = null, + otherPartyAccountId: String? = null, bookingText: String? = null): AccountTransaction { + return AccountTransaction( + AccountId, amount, Currency, reference, BookingDate, ValueDate, otherPartyName, otherPartyBankCode, otherPartyAccountId, bookingText + ) + } + + + private fun countOccurrences(string: String, characterToFind: Char): Int { + var countOccurrences = 0 + + for (char in string) { + if (char == characterToFind) { + countOccurrences++ + } + } + + return countOccurrences + } + + private fun calculateCountSeparators(countTransactions: Int): Int { + return (countTransactions + 1) * // + 1 cause of header row + (10 - 1) // - 1 cause for the last column no separator gets printed + } + + + private fun setLocale(locale: Locale) { + Locale.setDefault(Locale.Category.FORMAT, locale) + } +} \ No newline at end of file diff --git a/ui/BankingAndroidApp/build.gradle b/ui/BankingAndroidApp/build.gradle index dea7fb2e..c3d634c2 100644 --- a/ui/BankingAndroidApp/build.gradle +++ b/ui/BankingAndroidApp/build.gradle @@ -97,6 +97,8 @@ dependencies { implementation project(':LuceneBankingPersistence') implementation project(':RoomBankingPersistence') + implementation project(':CsvAccountTransactionsImporterAndExporter') + implementation "net.dankito.text.extraction:itext2-text-extractor:$textExtractorVersion" implementation "net.dankito.text.extraction:pdfbox-android-text-extractor:$textExtractorVersion" diff --git a/ui/BankingAndroidApp/src/main/java/net/dankito/banking/ui/android/MainActivity.kt b/ui/BankingAndroidApp/src/main/java/net/dankito/banking/ui/android/MainActivity.kt index 71b114e6..6f61767c 100644 --- a/ui/BankingAndroidApp/src/main/java/net/dankito/banking/ui/android/MainActivity.kt +++ b/ui/BankingAndroidApp/src/main/java/net/dankito/banking/ui/android/MainActivity.kt @@ -40,12 +40,13 @@ class MainActivity : BaseActivity() { private lateinit var floatingActionMenuButton: MainActivityFloatingActionMenuButton - private val permissionsService: IPermissionsService = PermissionsService(this) - @Inject protected lateinit var presenter: BankingPresenter + @Inject + protected lateinit var permissionsService: IPermissionsService + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) diff --git a/ui/BankingAndroidApp/src/main/java/net/dankito/banking/ui/android/di/BankingComponent.kt b/ui/BankingAndroidApp/src/main/java/net/dankito/banking/ui/android/di/BankingComponent.kt index 961be4ea..b0c88668 100644 --- a/ui/BankingAndroidApp/src/main/java/net/dankito/banking/ui/android/di/BankingComponent.kt +++ b/ui/BankingAndroidApp/src/main/java/net/dankito/banking/ui/android/di/BankingComponent.kt @@ -10,6 +10,7 @@ import net.dankito.banking.ui.android.dialogs.EnterTanDialog import net.dankito.banking.ui.android.dialogs.SendMessageLogDialog import net.dankito.banking.ui.android.dialogs.TransferMoneyDialog import net.dankito.banking.ui.android.dialogs.settings.ProtectAppSettingsDialog +import net.dankito.banking.ui.android.dialogs.settings.SettingsDialog import net.dankito.banking.ui.android.dialogs.settings.SettingsDialogBase import net.dankito.banking.ui.android.home.HomeFragment import net.dankito.banking.ui.android.views.BiometricAuthenticationButton @@ -43,6 +44,8 @@ interface BankingComponent { fun inject(settingsDialogBase: SettingsDialogBase) + fun inject(settingsDialog: SettingsDialog) + fun inject(protectAppSettingsDialog: ProtectAppSettingsDialog) fun inject(biometricAuthenticationButton: BiometricAuthenticationButton) diff --git a/ui/BankingAndroidApp/src/main/java/net/dankito/banking/ui/android/di/BankingModule.kt b/ui/BankingAndroidApp/src/main/java/net/dankito/banking/ui/android/di/BankingModule.kt index ab7ffc57..84b4d42e 100644 --- a/ui/BankingAndroidApp/src/main/java/net/dankito/banking/ui/android/di/BankingModule.kt +++ b/ui/BankingAndroidApp/src/main/java/net/dankito/banking/ui/android/di/BankingModule.kt @@ -29,6 +29,8 @@ import net.dankito.text.extraction.TextExtractorRegistry import net.dankito.text.extraction.pdf.PdfBoxAndroidPdfTextExtractor import net.dankito.text.extraction.pdf.iText2PdfTextExtractor import net.dankito.utils.ThreadPool +import net.dankito.utils.android.permissions.IPermissionsService +import net.dankito.utils.android.permissions.PermissionsService import net.dankito.utils.web.client.IWebClient import net.dankito.utils.web.client.OkHttpWebClient import javax.inject.Named @@ -87,6 +89,13 @@ class BankingModule(private val applicationContext: Context) { } + @Provides + @Singleton + fun providePermissionsService() : IPermissionsService { + return PermissionsService(mainActivity) + } + + @Provides @Singleton fun provideAuthenticationService(biometricAuthenticationService: IBiometricAuthenticationService, persistence: IBankingPersistence, diff --git a/ui/BankingAndroidApp/src/main/java/net/dankito/banking/ui/android/dialogs/settings/SettingsDialog.kt b/ui/BankingAndroidApp/src/main/java/net/dankito/banking/ui/android/dialogs/settings/SettingsDialog.kt index 946cfeb8..3216542d 100644 --- a/ui/BankingAndroidApp/src/main/java/net/dankito/banking/ui/android/dialogs/settings/SettingsDialog.kt +++ b/ui/BankingAndroidApp/src/main/java/net/dankito/banking/ui/android/dialogs/settings/SettingsDialog.kt @@ -1,24 +1,41 @@ package net.dankito.banking.ui.android.dialogs.settings import android.os.Bundle -import android.view.* +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup import androidx.appcompat.content.res.AppCompatResources import androidx.fragment.app.FragmentActivity import kotlinx.android.synthetic.main.dialog_settings.* import kotlinx.android.synthetic.main.dialog_settings.view.* +import net.codinux.banking.tools.importerexporter.CsvAccountTransactionsExporter +import net.codinux.banking.tools.importerexporter.model.AccountTransaction import net.dankito.banking.ui.android.R import net.dankito.banking.ui.android.adapter.BankDataAdapterItem import net.dankito.banking.ui.android.adapter.FastAdapterRecyclerView +import net.dankito.banking.ui.android.di.BankingComponent +import net.dankito.banking.ui.model.IAccountTransaction import net.dankito.banking.ui.model.TypedBankData +import net.dankito.filechooserdialog.FileChooserDialog +import net.dankito.filechooserdialog.model.FileChooserDialogConfig +import net.dankito.utils.android.permissions.IPermissionsService +import java.io.File +import java.text.SimpleDateFormat +import javax.inject.Inject open class SettingsDialog : SettingsDialogBase() { companion object { + val ExportTransactionsDateFormat = SimpleDateFormat("yyyyMMdd") + const val DialogTag = "SettingsDialog" } + @Inject + protected lateinit var permissionsService: IPermissionsService + protected lateinit var banksAdapter: FastAdapterRecyclerView protected var banksChangedListener = { _: List -> @@ -26,6 +43,11 @@ open class SettingsDialog : SettingsDialogBase() { } + init { + BankingComponent.component.inject(this) + } + + fun show(activity: FragmentActivity) { show(activity, DialogTag) } @@ -59,6 +81,8 @@ open class SettingsDialog : SettingsDialogBase() { btnSetAppProtection.setOnClickListener { navigateToProtectAppSettingsDialog() } selectLockAppAfter.periodInMinutes = presenter.appSettings.lockAppAfterMinutes + btnExportAccountTransactions.setOnClickListener { exportAccountTransactions() } + // on Pre Lollipop devices setting vector drawables in xml is not supported -> set left drawable here val sendIcon = AppCompatResources.getDrawable(context, R.drawable.ic_baseline_send_24) btnShowSendMessageLogDialog.setCompoundDrawablesWithIntrinsicBounds(sendIcon, null, null, null) @@ -97,6 +121,62 @@ open class SettingsDialog : SettingsDialogBase() { } } + + protected open fun exportAccountTransactions() { + val initialDirectory = presenter.appSettings.lastSelectedExportFolder?.let { File(it) } + val suggestedFilename = getExportCsvSuggestedFilename() + +// val intent = Intent(Intent.ACTION_CREATE_DOCUMENT) +// intent.addCategory(Intent.CATEGORY_OPENABLE) +// intent.type = "text/csv" +// +// intent.putExtra(Intent.EXTRA_TITLE, suggestedFilename) +// +// startActivityForResult(intent, 1) + + activity?.let { activity -> + val config = FileChooserDialogConfig(initialDirectory = initialDirectory, suggestedFilenameForSaveFileDialog = suggestedFilename) + FileChooserDialog().showSaveFileInFullscreenDialog(activity, permissionsService, config) { _, selectedFile -> + selectedFile?.let { + val transactions = presenter.allTransactions.map { mapTransaction(it) } + + CsvAccountTransactionsExporter().export(selectedFile, transactions) + + presenter.appSettings.lastSelectedExportFolder = selectedFile.parentFile.absolutePath + presenter.appSettingsChanged() + } + } + } + } + + // TODO: this is almost the same code as in JavaFX MainMenuBar.getExportCsvSuggestedFilename() -> merge + protected open fun getExportCsvSuggestedFilename(): String? { + val transactions = presenter.allTransactions + val transactionsDates = transactions.map { it.valueDate } + val transactionsStartDate = transactionsDates.min() + val transactionsEndDate = transactionsDates.max() + + return context?.getString(R.string.dialog_settings_export_account_transactions_suggested_file_name, + transactionsStartDate?.let { ExportTransactionsDateFormat.format(it) } ?: "", transactionsEndDate?.let { ExportTransactionsDateFormat.format(it) } ?: "") + } + + // TODO: this is exactly the same code as in JavaFX MainMenuBar.mapTransaction() -> merge + protected open fun mapTransaction(transaction: IAccountTransaction): AccountTransaction { + return AccountTransaction( + transaction.account.iban ?: transaction.account.identifier, + transaction.amount, + transaction.currency, + transaction.reference, + transaction.bookingDate, + transaction.valueDate, + transaction.otherPartyName, + transaction.otherPartyBankCode, + transaction.otherPartyAccountId, + transaction.bookingText + ) + } + + protected open fun reorderedBanks(oldPosition: Int, oldItem: TypedBankData, newPosition: Int, newItem: TypedBankData) { oldItem.displayIndex = oldPosition newItem.displayIndex = newPosition diff --git a/ui/BankingAndroidApp/src/main/res/layout/dialog_settings.xml b/ui/BankingAndroidApp/src/main/res/layout/dialog_settings.xml index 838be146..c93a88a1 100644 --- a/ui/BankingAndroidApp/src/main/res/layout/dialog_settings.xml +++ b/ui/BankingAndroidApp/src/main/res/layout/dialog_settings.xml @@ -115,6 +115,21 @@ /> +