From ac06a3d70894550192c7f29c5d70ed93e92e58e6 Mon Sep 17 00:00:00 2001 From: Severiano Jaramillo Date: Fri, 27 May 2022 22:24:26 -0700 Subject: [PATCH] Export PDF transactions without requiring storage permission. --- app/build.gradle | 2 + .../usecase/ExportTransactionsToPdfUseCase.kt | 166 ++++++++++++++ .../transactions}/FilterOptions.kt | 3 +- .../transactions}/FilterOptionsDialog.kt | 7 +- .../transactions}/TransactionsFragment.kt | 103 +++------ .../transactions}/TransactionsViewModel.kt | 67 ++++-- .../utils/PDFGeneratorTask.kt | 215 ------------------ .../main/res/navigation/mobile_navigation.xml | 2 +- app/src/main/res/values-de/strings.xml | 1 - app/src/main/res/values-es/strings.xml | 1 - app/src/main/res/values-fr/strings.xml | 1 - app/src/main/res/values-hi/strings.xml | 1 - app/src/main/res/values-ja/strings.xml | 1 - app/src/main/res/values-pa/strings.xml | 1 - app/src/main/res/values-pt/strings.xml | 1 - app/src/main/res/values-ru/strings.xml | 1 - app/src/main/res/values-zh/strings.xml | 1 - app/src/main/res/values/strings.xml | 1 - 18 files changed, 249 insertions(+), 326 deletions(-) create mode 100644 app/src/main/java/cy/agorise/bitsybitshareswallet/domain/usecase/ExportTransactionsToPdfUseCase.kt rename app/src/main/java/cy/agorise/bitsybitshareswallet/{models => ui/transactions}/FilterOptions.kt (83%) rename app/src/main/java/cy/agorise/bitsybitshareswallet/{fragments => ui/transactions}/FilterOptionsDialog.kt (98%) rename app/src/main/java/cy/agorise/bitsybitshareswallet/{fragments => ui/transactions}/TransactionsFragment.kt (66%) rename app/src/main/java/cy/agorise/bitsybitshareswallet/{viewmodels => ui/transactions}/TransactionsViewModel.kt (71%) delete mode 100644 app/src/main/java/cy/agorise/bitsybitshareswallet/utils/PDFGeneratorTask.kt diff --git a/app/build.gradle b/app/build.gradle index 233f067..300529f 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -74,8 +74,10 @@ dependencies { implementation project(':PDFJet') implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" // AndroidX + implementation 'androidx.activity:activity-ktx:1.2.4' implementation 'androidx.appcompat:appcompat:1.2.0' implementation 'androidx.constraintlayout:constraintlayout:2.0.4' + implementation "androidx.fragment:fragment-ktx:1.3.2" implementation "androidx.preference:preference-ktx:$preference_version" // Google implementation 'com.google.zxing:core:3.4.0' diff --git a/app/src/main/java/cy/agorise/bitsybitshareswallet/domain/usecase/ExportTransactionsToPdfUseCase.kt b/app/src/main/java/cy/agorise/bitsybitshareswallet/domain/usecase/ExportTransactionsToPdfUseCase.kt new file mode 100644 index 0000000..423fb1a --- /dev/null +++ b/app/src/main/java/cy/agorise/bitsybitshareswallet/domain/usecase/ExportTransactionsToPdfUseCase.kt @@ -0,0 +1,166 @@ +package cy.agorise.bitsybitshareswallet.domain.usecase + +import android.content.Context +import android.util.Log +import androidx.core.os.ConfigurationCompat +import androidx.documentfile.provider.DocumentFile +import com.pdfjet.* +import cy.agorise.bitsybitshareswallet.R +import cy.agorise.bitsybitshareswallet.database.joins.TransferDetail +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import java.io.BufferedOutputStream +import java.text.SimpleDateFormat +import java.util.* + +class ExportTransactionsToPdfUseCase(private val applicationContext: Context) { + + suspend operator fun invoke( + transactions: List, + folderDocumentFile: DocumentFile + ) = withContext(Dispatchers.IO) { // TODO Inject Dispatcher + // Create the PDF file name + val fileName = applicationContext.resources.let { + "${it.getString(R.string.app_name)}-${it.getString(R.string.title_transactions)}.pdf" + } + + // Obtains the path to store the PDF + val pdfDocUri = folderDocumentFile.createFile("application/pdf", fileName)?.uri + val contentResolver = applicationContext.contentResolver + val pdfOutputStream = contentResolver.openOutputStream(pdfDocUri!!, "w") + + // Creates a new PDF object + val pdf = PDF(BufferedOutputStream(pdfOutputStream), Compliance.PDF_A_1B) + + // Font used for the table headers + val f1 = Font(pdf, CoreFont.HELVETICA_BOLD).apply { size = 7f } + + // Font used for the table contents + val f2 = Font(pdf, CoreFont.HELVETICA).apply { size = 7f } + + // Creates a new PDF table + val table = Table() + + // 2D array of cells used to populate the PDF table + val tableData = mutableListOf>() + + // Add column names/headers + val columnNames = intArrayOf(R.string.title_from, R.string.title_to, R.string.title_memo, R.string.title_date, + R.string.title_time, R.string.title_amount, R.string.title_equivalent_value) + + val header = columnNames.map { columnName -> + Cell(f1, applicationContext.getString(columnName)).apply { setPadding(2f) } + } + + // Add the table headers + tableData.add(header) + + // Add the table contents + applicationContext.let { context -> + val locale = ConfigurationCompat.getLocales(context.resources.configuration)[0] + tableData.addAll(getData(transactions, f2, locale)) + } + + // Configure the PDF table + table.setData(tableData, Table.DATA_HAS_1_HEADER_ROWS) + table.setCellBordersWidth(0.2f) + // The A4 size has 595 points of width, with the below we are trying to assign the same + // width to all cells and also keep them centered. + for (i in 0..6) { + table.setColumnWidth(i, 65f) + } + table.wrapAroundCellText() + table.mergeOverlaidBorders() + + // Populate the PDF table + while (table.hasMoreData()) { + // Configures the PDF page + val page = Page(pdf, Letter.PORTRAIT) + table.setLocation(45f, 30f) + table.drawOn(page) + } + + pdf.close() + + Log.d("ExportTransactionsToPdf","PDF generated and saved") + } + + private suspend fun getData( + transferDetails: List, + font: Font, + locale: Locale + ): List> = withContext(Dispatchers.Default) { // TODO Inject Dispatcher + + val tableData = mutableListOf>() + + // Configure date and time formats to reuse in all the transfers + val dateFormat = SimpleDateFormat("MM-dd-yyyy", locale) + val timeFormat = SimpleDateFormat("HH:mm:ss", locale) + var date: Date + + // Save all the transfers information + for ((index, transferDetail) in transferDetails.withIndex()) { + val cols = mutableListOf() + + date = Date(transferDetail.date * 1000) + + cols.add(transferDetail.from ?: "") // From + cols.add(transferDetail.to ?: "") // To + cols.add(transferDetail.memo) // Memo + cols.add(dateFormat.format(date)) // Date + cols.add(timeFormat.format(date)) // Time + + // Asset Amount + val assetPrecision = transferDetail.assetPrecision + val assetAmount = + transferDetail.assetAmount.toDouble() / Math.pow(10.0, assetPrecision.toDouble()) + cols.add( + String.format( + "%.${assetPrecision}f %s", + assetAmount, + transferDetail.assetSymbol + ) + ) + + // Fiat Equivalent + if (transferDetail.fiatAmount != null && transferDetail.fiatSymbol != null) { + val currency = Currency.getInstance(transferDetail.fiatSymbol) + val fiatAmount = transferDetail.fiatAmount.toDouble() / + Math.pow(10.0, currency.defaultFractionDigits.toDouble()) + cols.add( + String.format( + "%.${currency.defaultFractionDigits}f %s", + fiatAmount, currency.currencyCode + ) + ) + } + + val row = cols.map { col -> + Cell(font, col).apply { setPadding(2f) } + } + + tableData.add(row) + + appendMissingCells(tableData, font) + + // TODO update progress + } + + tableData + } + + private fun appendMissingCells(tableData: List>, font: Font) { + val firstRow = tableData[0] + val numOfColumns = firstRow.size + for (i in tableData.indices) { + val dataRow = tableData[i] as ArrayList + val dataRowColumns = dataRow.size + if (dataRowColumns < numOfColumns) { + for (j in 0 until numOfColumns - dataRowColumns) { + dataRow.add(Cell(font)) + } + dataRow[dataRowColumns - 1].colSpan = numOfColumns - dataRowColumns + 1 + } + } + } +} diff --git a/app/src/main/java/cy/agorise/bitsybitshareswallet/models/FilterOptions.kt b/app/src/main/java/cy/agorise/bitsybitshareswallet/ui/transactions/FilterOptions.kt similarity index 83% rename from app/src/main/java/cy/agorise/bitsybitshareswallet/models/FilterOptions.kt rename to app/src/main/java/cy/agorise/bitsybitshareswallet/ui/transactions/FilterOptions.kt index 1f03878..b420a41 100644 --- a/app/src/main/java/cy/agorise/bitsybitshareswallet/models/FilterOptions.kt +++ b/app/src/main/java/cy/agorise/bitsybitshareswallet/ui/transactions/FilterOptions.kt @@ -1,7 +1,6 @@ -package cy.agorise.bitsybitshareswallet.models +package cy.agorise.bitsybitshareswallet.ui.transactions import android.os.Parcelable -import cy.agorise.bitsybitshareswallet.fragments.TransactionsFragment import kotlinx.parcelize.Parcelize /** diff --git a/app/src/main/java/cy/agorise/bitsybitshareswallet/fragments/FilterOptionsDialog.kt b/app/src/main/java/cy/agorise/bitsybitshareswallet/ui/transactions/FilterOptionsDialog.kt similarity index 98% rename from app/src/main/java/cy/agorise/bitsybitshareswallet/fragments/FilterOptionsDialog.kt rename to app/src/main/java/cy/agorise/bitsybitshareswallet/ui/transactions/FilterOptionsDialog.kt index c040519..3361e62 100644 --- a/app/src/main/java/cy/agorise/bitsybitshareswallet/fragments/FilterOptionsDialog.kt +++ b/app/src/main/java/cy/agorise/bitsybitshareswallet/ui/transactions/FilterOptionsDialog.kt @@ -1,4 +1,4 @@ -package cy.agorise.bitsybitshareswallet.fragments +package cy.agorise.bitsybitshareswallet.ui.transactions import android.content.res.Resources @@ -18,7 +18,6 @@ import com.google.firebase.crashlytics.FirebaseCrashlytics import cy.agorise.bitsybitshareswallet.adapters.BalancesDetailsAdapter import cy.agorise.bitsybitshareswallet.database.joins.BalanceDetail import cy.agorise.bitsybitshareswallet.databinding.DialogFilterOptionsBinding -import cy.agorise.bitsybitshareswallet.models.FilterOptions import cy.agorise.bitsybitshareswallet.utils.Constants import cy.agorise.bitsybitshareswallet.utils.Helper import cy.agorise.bitsybitshareswallet.viewmodels.BalanceDetailViewModel @@ -110,7 +109,7 @@ class FilterOptionsDialog : DialogFragment() { binding.cbAsset.isChecked = mFilterOptions.assetAll // Configure BalanceDetailViewModel to obtain the user's Balances - viewModel.getAll().observe(viewLifecycleOwner, { balancesDetails -> + viewModel.getAll().observe(viewLifecycleOwner) { balancesDetails -> mBalanceDetails.clear() mBalanceDetails.addAll(balancesDetails) mBalanceDetails.sortWith { a, b -> a.toString().compareTo(b.toString(), true) } @@ -128,7 +127,7 @@ class FilterOptionsDialog : DialogFragment() { break } } - }) + } // Initialize Equivalent Value binding.cbEquivalentValue.setOnCheckedChangeListener { _, isChecked -> diff --git a/app/src/main/java/cy/agorise/bitsybitshareswallet/fragments/TransactionsFragment.kt b/app/src/main/java/cy/agorise/bitsybitshareswallet/ui/transactions/TransactionsFragment.kt similarity index 66% rename from app/src/main/java/cy/agorise/bitsybitshareswallet/fragments/TransactionsFragment.kt rename to app/src/main/java/cy/agorise/bitsybitshareswallet/ui/transactions/TransactionsFragment.kt index c117d65..9c228f8 100644 --- a/app/src/main/java/cy/agorise/bitsybitshareswallet/fragments/TransactionsFragment.kt +++ b/app/src/main/java/cy/agorise/bitsybitshareswallet/ui/transactions/TransactionsFragment.kt @@ -1,13 +1,10 @@ -package cy.agorise.bitsybitshareswallet.fragments +package cy.agorise.bitsybitshareswallet.ui.transactions -import android.Manifest -import android.content.pm.PackageManager import android.graphics.Point import android.os.Bundle -import android.os.Environment import android.view.* +import androidx.activity.result.contract.ActivityResultContracts.OpenDocumentTree import androidx.appcompat.widget.SearchView -import androidx.core.content.ContextCompat import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentActivity import androidx.fragment.app.viewModels @@ -20,12 +17,10 @@ import com.jakewharton.rxbinding3.appcompat.queryTextChangeEvents import cy.agorise.bitsybitshareswallet.R import cy.agorise.bitsybitshareswallet.adapters.TransfersDetailsAdapter import cy.agorise.bitsybitshareswallet.databinding.FragmentTransactionsBinding -import cy.agorise.bitsybitshareswallet.models.FilterOptions -import cy.agorise.bitsybitshareswallet.utils.* -import cy.agorise.bitsybitshareswallet.viewmodels.TransactionsViewModel +import cy.agorise.bitsybitshareswallet.utils.BounceTouchListener +import cy.agorise.bitsybitshareswallet.utils.Constants import io.reactivex.android.schedulers.AndroidSchedulers import io.reactivex.disposables.CompositeDisposable -import java.io.File import java.util.* import java.util.concurrent.TimeUnit @@ -35,17 +30,14 @@ import java.util.concurrent.TimeUnit */ class TransactionsFragment : Fragment(), FilterOptionsDialog.OnFilterOptionsSelectedListener { - companion object { - private const val TAG = "TransactionsFragment" - - private const val REQUEST_WRITE_EXTERNAL_STORAGE_PERMISSION = 100 - } - private val viewModel: TransactionsViewModel by viewModels() private var _binding: FragmentTransactionsBinding? = null private val binding get() = _binding!! + private var isPdfRequested: Boolean = false + private var isCsvRequested: Boolean = false + private var mDisposables = CompositeDisposable() override fun onCreateView( @@ -78,7 +70,7 @@ class TransactionsFragment : Fragment(), FilterOptionsDialog.OnFilterOptionsSele binding.rvTransactions.layoutManager = LinearLayoutManager(context) // Configure TransactionsViewModel to fetch the transaction history - viewModel.getFilteredTransactions(userId).observe(viewLifecycleOwner, { transactions -> + viewModel.getFilteredTransactions(userId).observe(viewLifecycleOwner) { transactions -> if (transactions.isEmpty()) { binding.rvTransactions.visibility = View.GONE binding.tvEmpty.visibility = View.VISIBLE @@ -94,7 +86,7 @@ class TransactionsFragment : Fragment(), FilterOptionsDialog.OnFilterOptionsSele if (shouldScrollUp) binding.rvTransactions.scrollToPosition(0) } - }) + } // Set custom touch listener to handle bounce/stretch effect val bounceTouchListener = BounceTouchListener(binding.rvTransactions) @@ -136,7 +128,7 @@ class TransactionsFragment : Fragment(), FilterOptionsDialog.OnFilterOptionsSele true } R.id.menu_export -> { - verifyStoragePermission() + showExportOptionsDialog() true } else -> super.onOptionsItemSelected(item) @@ -162,75 +154,28 @@ class TransactionsFragment : Fragment(), FilterOptionsDialog.OnFilterOptionsSele viewModel.applyFilterOptions(filterOptions) } - /** Verifies that the storage permission has been granted before attempting to generate the export options */ - private fun verifyStoragePermission() { - if (ContextCompat.checkSelfPermission( - requireActivity(), - Manifest.permission.WRITE_EXTERNAL_STORAGE - ) - != PackageManager.PERMISSION_GRANTED - ) { - // Permission is not already granted - requestPermissions( - arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE), - REQUEST_WRITE_EXTERNAL_STORAGE_PERMISSION - ) - } else { - // Permission is already granted - showExportOptionsDialog() - } - } - - /** Received the result of the storage permission request and if it was accepted then shows the export options - * dialog, but if it was not accepted then shows a toast explaining that the permission is necessary to generate - * the export options */ - override fun onRequestPermissionsResult( - requestCode: Int, - permissions: Array, - grantResults: IntArray - ) { - super.onRequestPermissionsResult(requestCode, permissions, grantResults) - if (requestCode == REQUEST_WRITE_EXTERNAL_STORAGE_PERMISSION) { - if ((grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED)) { - showExportOptionsDialog() - } else { - context?.toast(getString(R.string.msg__storage_permission_necessary_export)) - } - return - } - } - private fun showExportOptionsDialog() { + // Make export options selected by default + val indices = intArrayOf(0, 1) + MaterialDialog(requireContext()).show { title(R.string.title_export_transactions) listItemsMultiChoice( R.array.export_options, - initialSelection = intArrayOf(0, 1) + initialSelection = indices, + waitForPositiveButton = true ) { _, indices, _ -> - val exportPDF = indices.contains(0) - val exportCSV = indices.contains(1) - exportFilteredTransactions(exportPDF, exportCSV) + // Update export options selected + isPdfRequested = indices.contains(0) + isCsvRequested = indices.contains(1) } - positiveButton(R.string.title_export) + negativeButton(android.R.string.cancel) { dismiss() } + positiveButton(R.string.title_export) { getFolderForExport.launch(null) } } } - /** Creates the export procedures for PDF and CSV, depending on the user selection. */ - private fun exportFilteredTransactions(exportPDF: Boolean, exportCSV: Boolean) { - // Verifies the BiTSy folder exists in the external storage and if it doesn't then it tries to create it - val dir = File(Environment.getExternalStorageDirectory(), Constants.EXTERNAL_STORAGE_FOLDER) - if (!dir.exists()) { - if (!dir.mkdirs()) - return - } - - viewModel.getFilteredTransactionsOnce()?.let { filteredTransactions -> - if (exportPDF) - activity?.let { PDFGeneratorTask(it).execute(filteredTransactions) } - - if (exportCSV) - activity?.let { CSVGenerationTask(it).execute(filteredTransactions) } - } + private val getFolderForExport = registerForActivityResult(OpenDocumentTree()) { folderUri -> + viewModel.exportFilteredTransactions(folderUri, isPdfRequested, isCsvRequested) } override fun onDestroy() { @@ -238,4 +183,8 @@ class TransactionsFragment : Fragment(), FilterOptionsDialog.OnFilterOptionsSele if (!mDisposables.isDisposed) mDisposables.dispose() } + + companion object { + private const val TAG = "TransactionsFragment" + } } diff --git a/app/src/main/java/cy/agorise/bitsybitshareswallet/viewmodels/TransactionsViewModel.kt b/app/src/main/java/cy/agorise/bitsybitshareswallet/ui/transactions/TransactionsViewModel.kt similarity index 71% rename from app/src/main/java/cy/agorise/bitsybitshareswallet/viewmodels/TransactionsViewModel.kt rename to app/src/main/java/cy/agorise/bitsybitshareswallet/ui/transactions/TransactionsViewModel.kt index 7a1f952..a81a96e 100644 --- a/app/src/main/java/cy/agorise/bitsybitshareswallet/viewmodels/TransactionsViewModel.kt +++ b/app/src/main/java/cy/agorise/bitsybitshareswallet/ui/transactions/TransactionsViewModel.kt @@ -1,10 +1,13 @@ -package cy.agorise.bitsybitshareswallet.viewmodels +package cy.agorise.bitsybitshareswallet.ui.transactions import android.app.Application +import android.net.Uri +import android.util.Log +import androidx.documentfile.provider.DocumentFile import androidx.lifecycle.* import com.google.android.material.datepicker.MaterialDatePicker import cy.agorise.bitsybitshareswallet.database.joins.TransferDetail -import cy.agorise.bitsybitshareswallet.models.FilterOptions +import cy.agorise.bitsybitshareswallet.domain.usecase.ExportTransactionsToPdfUseCase import cy.agorise.bitsybitshareswallet.repositories.TransferDetailRepository import cy.agorise.bitsybitshareswallet.utils.Helper import kotlinx.coroutines.Dispatchers @@ -14,17 +17,17 @@ import java.util.* class TransactionsViewModel(application: Application) : AndroidViewModel(application) { - companion object { - const val TAG = "TransactionsViewModel" - } - private var mRepository = TransferDetailRepository(application) + + // TODO Inject below dependencies + private val exportTransactionsToPdfUseCase = ExportTransactionsToPdfUseCase(application) + private val mRepository = TransferDetailRepository(application) /** * [FilterOptions] used to filter the list of [TransferDetail] taken from the database */ private var mFilterOptions = FilterOptions() - private lateinit var transactions : LiveData> + private lateinit var transactions: LiveData> /** * This [MediatorLiveData] is used to combine two sources of information into one, keeping the @@ -64,11 +67,12 @@ class TransactionsViewModel(application: Application) : AndroidViewModel(applica return mFilterOptions } - internal fun applyFilterOptions(filterOptions: FilterOptions) = transactions.value?.let { transactions -> - viewModelScope.launch { - filteredTransactions.value = filter(transactions, filterOptions) - } - }.also { mFilterOptions = filterOptions } + internal fun applyFilterOptions(filterOptions: FilterOptions) = + transactions.value?.let { transactions -> + viewModelScope.launch { + filteredTransactions.value = filter(transactions, filterOptions) + } + }.also { mFilterOptions = filterOptions } internal fun setFilterQuery(query: String) = transactions.value?.let { transactions -> mFilterOptions.query = query @@ -83,7 +87,10 @@ class TransactionsViewModel(application: Application) : AndroidViewModel(applica * Filters the given list of [TransferDetail] given the [FilterOptions] and returns a filtered list * of [TransferDetail], doing all the work in a background thread using kotlin coroutines */ - private suspend fun filter(transactions: List, filterOptions: FilterOptions) : List { + private suspend fun filter( + transactions: List, + filterOptions: FilterOptions + ): List { return withContext(Dispatchers.Default) { // Create a list to store the filtered transactions @@ -107,7 +114,8 @@ class TransactionsViewModel(application: Application) : AndroidViewModel(applica // Filter by date range if (!filterOptions.dateRangeAll && (transaction.date < startDate || - transaction.date > endDate)) + transaction.date > endDate) + ) continue // Filter by asset @@ -115,8 +123,10 @@ class TransactionsViewModel(application: Application) : AndroidViewModel(applica continue // Filter by equivalent value - if (!filterOptions.equivalentValueAll && ((transaction.fiatAmount ?: -1 ) < filterOptions.fromEquivalentValue - || (transaction.fiatAmount ?: -1) > filterOptions.toEquivalentValue)) + if (!filterOptions.equivalentValueAll && ((transaction.fiatAmount + ?: -1) < filterOptions.fromEquivalentValue + || (transaction.fiatAmount ?: -1) > filterOptions.toEquivalentValue) + ) continue // Filter transactions sent to agorise @@ -133,4 +143,27 @@ class TransactionsViewModel(application: Application) : AndroidViewModel(applica filteredTransactions } } -} \ No newline at end of file + + /** Creates the export procedures for PDF and CSV, depending on the user selection. */ + fun exportFilteredTransactions(folderUri: Uri, exportPdf: Boolean, exportCsv: Boolean) { + Log.d(TAG, "Export options selected. PDF: $exportPdf, CSV: $exportCsv") + + if (!exportPdf && !exportCsv) return + + // TODO Use injected context + val folderDocumentFile = DocumentFile.fromTreeUri(getApplication(), folderUri) ?: return + + if (exportPdf) { + viewModelScope.launch { + filteredTransactions.value?.let { transactions -> + // TODO Show success/failure message + exportTransactionsToPdfUseCase(transactions, folderDocumentFile) + } + } + } + } + + companion object { + private const val TAG = "TransactionsViewModel" + } +} diff --git a/app/src/main/java/cy/agorise/bitsybitshareswallet/utils/PDFGeneratorTask.kt b/app/src/main/java/cy/agorise/bitsybitshareswallet/utils/PDFGeneratorTask.kt deleted file mode 100644 index ad7b995..0000000 --- a/app/src/main/java/cy/agorise/bitsybitshareswallet/utils/PDFGeneratorTask.kt +++ /dev/null @@ -1,215 +0,0 @@ -package cy.agorise.bitsybitshareswallet.utils - -import android.content.Context -import android.os.AsyncTask -import android.os.Environment -import android.util.Log -import androidx.core.os.ConfigurationCompat -import com.pdfjet.* - -import cy.agorise.bitsybitshareswallet.R -import cy.agorise.bitsybitshareswallet.database.joins.TransferDetail -import java.io.* -import java.lang.Exception -import java.lang.ref.WeakReference -import java.text.SimpleDateFormat -import java.util.* - -/** - * AsyncTask subclass used to move the PDF generation procedure to a background thread - * and inform the UI of the progress. - */ -class PDFGeneratorTask(context: Context) : AsyncTask, Int, String>() { - - companion object { - private const val TAG = "PDFGeneratorTask" - } - - private val mContextRef: WeakReference = WeakReference(context) - - override fun doInBackground(vararg params: List): String { - return createPDFDocument(params[0]) - } - - private fun combinePath(path1: String, path2: String): String { - val file1 = File(path1) - val file2 = File(file1, path2) - return file2.path - } - - /** Creates an empty file with the given name, in case it does not exist */ - private fun createEmptyFile(path: String) { - try { - val file = File(path) - val writer = FileWriter(file) - writer.flush() - writer.close() - } catch (e: IOException) { - e.printStackTrace() - } - - } - - private fun createPDFDocument(transferDetails: List): String { - return try { - // Create the PDF file name - val fileName = mContextRef.get()?.resources?.let { - "${it.getString(R.string.app_name)}-${it.getString(R.string.title_transactions)}"} + ".pdf" - - // Obtains the path to store the PDF - val externalStorageFolder = Environment.getExternalStorageDirectory().absolutePath + File.separator + - Constants.EXTERNAL_STORAGE_FOLDER - val filePath = combinePath(externalStorageFolder, fileName) - createEmptyFile(filePath) - - // Creates a new PDF object - val pdf = PDF( - BufferedOutputStream( - FileOutputStream(filePath)), Compliance.PDF_A_1B) - - - // Font used for the table headers - val f1 = Font(pdf, CoreFont.HELVETICA_BOLD) - f1.size = 7f - - // Font used for the table contents - val f2 = Font(pdf, CoreFont.HELVETICA) - f2.size = 7f - - // Creates a new PDF table - val table = Table() - - // 2D array of cells used to populate the PDF table - val tableData = ArrayList>() - - // Add column names/headers - val columnNames = intArrayOf(R.string.title_from, R.string.title_to, R.string.title_memo, R.string.title_date, - R.string.title_time, R.string.title_amount, R.string.title_equivalent_value) - - val header = ArrayList() - - for (columnName in columnNames) { - val cell = Cell(f1, mContextRef.get()?.getString(columnName)) - cell.setTopPadding(2f) - cell.setBottomPadding(2f) - cell.setLeftPadding(2f) - cell.setRightPadding(2f) - header.add(cell) - } - - // Add the table headers - tableData.add(header) - - // Add the table contents - mContextRef.get()?.let { context -> - val locale = ConfigurationCompat.getLocales(context.resources.configuration)[0] - tableData.addAll(getData(transferDetails, f2, locale)) - } - - // Configure the PDF table - table.setData(tableData, Table.DATA_HAS_1_HEADER_ROWS) - table.setCellBordersWidth(0.2f) - // The A4 size has 595 points of width, with the below we are trying to assign the same - // width to all cells and also keep them centered. - for (i in 0..6) { - table.setColumnWidth(i, 65f) - } - table.wrapAroundCellText() - table.mergeOverlaidBorders() - - // Populate the PDF table - while (table.hasMoreData()) { - // Configures the PDF page - val page = Page(pdf, Letter.PORTRAIT) - table.setLocation(45f, 30f) - table.drawOn(page) - } - - pdf.close() - - "PDF generated and saved: $filePath" - } catch (e: Exception) { - Log.e(TAG, "Exception while trying to generate a PDF. Msg: " + e.message) - "Unable to generate PDF. Please retry. Error: ${e.message}" - } - } - - private fun getData(transferDetails: List, font: Font, locale: Locale): List> { - - val tableData = ArrayList>() - - // Configure date and time formats to reuse in all the transfers - val dateFormat = SimpleDateFormat("MM-dd-yyyy", locale) - val timeFormat = SimpleDateFormat("HH:mm:ss", locale) - var date: Date - - // Save all the transfers information - for ( (index, transferDetail) in transferDetails.withIndex()) { - val row = ArrayList() - - val cols = ArrayList() - - date = Date(transferDetail.date * 1000) - - cols.add(transferDetail.from ?: "") // From - cols.add(transferDetail.to ?: "") // To - cols.add(transferDetail.memo) // Memo - cols.add(dateFormat.format(date)) // Date - cols.add(timeFormat.format(date)) // Time - - // Asset Amount - val assetPrecision = transferDetail.assetPrecision - val assetAmount = transferDetail.assetAmount.toDouble() / Math.pow(10.0, assetPrecision.toDouble()) - cols.add(String.format("%.${assetPrecision}f %s", assetAmount, transferDetail.assetSymbol)) - - // Fiat Equivalent - if (transferDetail.fiatAmount != null && transferDetail.fiatSymbol != null) { - val currency = Currency.getInstance(transferDetail.fiatSymbol) - val fiatAmount = transferDetail.fiatAmount.toDouble() / - Math.pow(10.0, currency.defaultFractionDigits.toDouble()) - cols.add(String.format("%.${currency.defaultFractionDigits}f %s", - fiatAmount, currency.currencyCode)) - } - - for (col in cols) { - val cell = Cell(font, col) - cell.setTopPadding(2f) - cell.setBottomPadding(2f) - cell.setLeftPadding(2f) - cell.setRightPadding(2f) - row.add(cell) - } - tableData.add(row) - - appendMissingCells(tableData, font) - - // TODO update progress - } - - return tableData - } - - private fun appendMissingCells(tableData: List>, font: Font) { - val firstRow = tableData[0] - val numOfColumns = firstRow.size - for (i in tableData.indices) { - val dataRow = tableData[i] as ArrayList - val dataRowColumns = dataRow.size - if (dataRowColumns < numOfColumns) { - for (j in 0 until numOfColumns - dataRowColumns) { - dataRow.add(Cell(font)) - } - dataRow[dataRowColumns - 1].colSpan = numOfColumns - dataRowColumns + 1 - } - } - } - - - override fun onProgressUpdate(values: Array) { - // TODO show progress - } - - override fun onPostExecute(message: String) { - mContextRef.get()?.toast(message) - } -} \ No newline at end of file diff --git a/app/src/main/res/navigation/mobile_navigation.xml b/app/src/main/res/navigation/mobile_navigation.xml index 82400b7..419f46c 100644 --- a/app/src/main/res/navigation/mobile_navigation.xml +++ b/app/src/main/res/navigation/mobile_navigation.xml @@ -50,7 +50,7 @@ diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 9585d3b..496a6c3 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -65,7 +65,6 @@ Exportieren Sie gefilterte Transaktionen PDF CSV - Zum Exportieren von PDF / CSV-Dateien ist eine Speichererlaubnis erforderlich. Von Zu Memo diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index f5a0286..3a83673 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -65,7 +65,6 @@ Exportar transacciones filtradas PDF CSV - El permiso de almacenamiento es necesario para exportar los archivos PDF/CSV. De Para Memo diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 1493c4d..88011ef 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -65,7 +65,6 @@ Exporter les transactions filtrées PDF CSV - Une autorisation de stockage est nécessaire pour exporter des fichiers PDF / CSV. De À Note diff --git a/app/src/main/res/values-hi/strings.xml b/app/src/main/res/values-hi/strings.xml index eeeb6a1..406aaca 100644 --- a/app/src/main/res/values-hi/strings.xml +++ b/app/src/main/res/values-hi/strings.xml @@ -65,7 +65,6 @@ फ़िल्टर्ड लेनदेन को निर्यात करें पीडीएफ सीएसवी - PDF / CSV फ़ाइलों को निर्यात करने के लिए भंडारण अनुमति आवश्यक है। से सेवा मेरे मेमो diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index 88f9a58..88bd8f7 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -65,7 +65,6 @@ フィルタリングされたトランザクションをエクスポートする PDF CSV - PDF / CSVファイルをエクスポートするには、ストレージ許可が必要です。 から メモ diff --git a/app/src/main/res/values-pa/strings.xml b/app/src/main/res/values-pa/strings.xml index 8119bb9..449942f 100644 --- a/app/src/main/res/values-pa/strings.xml +++ b/app/src/main/res/values-pa/strings.xml @@ -65,7 +65,6 @@ ਫਿਲਟਰ ਟ੍ਰਾਂਜੈਕਸ਼ਨਾਂ ਨੂੰ ਐਕਸਪੋਰਟ ਕਰੋ ਪੀਡੀਐਫ CSV - ਸਟੋਰੇਜ਼ ਦੀ ਇਜ਼ਾਜ਼ਤ PDF / CSV ਫਾਈਲਾਂ ਨੂੰ ਨਿਰਯਾਤ ਕਰਨ ਲਈ ਜ਼ਰੂਰੀ ਹੈ. ਤੋਂ ਨੂੰ ਮੀਮੋ diff --git a/app/src/main/res/values-pt/strings.xml b/app/src/main/res/values-pt/strings.xml index 57520ef..7117c8b 100644 --- a/app/src/main/res/values-pt/strings.xml +++ b/app/src/main/res/values-pt/strings.xml @@ -65,7 +65,6 @@ Exportar transações filtradas PDF CSV - É necessária permissão de armazenamento para exportar arquivos PDF / CSV. De Para Memorando diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index e2c93d5..6d1c941 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -65,7 +65,6 @@ Экспорт отфильтрованных транзакций PDF CSV - Разрешение на хранение необходимо для экспорта файлов PDF / CSV. От к напоминание diff --git a/app/src/main/res/values-zh/strings.xml b/app/src/main/res/values-zh/strings.xml index 3d645e3..45f3ec8 100644 --- a/app/src/main/res/values-zh/strings.xml +++ b/app/src/main/res/values-zh/strings.xml @@ -65,7 +65,6 @@ 导出已过滤的交易 PDF CSV - 导出PDF / CSV文件需要存储权限。 备忘录 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 0109835..4dba3c3 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -67,7 +67,6 @@ Export filtered transactions PDF CSV - Storage permission is necessary to export PDF/CSV files. From To Memo