From d7c728db2f7599a7150744039d74dcc04c36efb3 Mon Sep 17 00:00:00 2001 From: Severiano Jaramillo Date: Tue, 5 Feb 2019 10:50:40 -0600 Subject: [PATCH] Added the libraries to create PDF and CSV files directly from the app. Verify that the BiTSy folder exists in the external storage and if it doesn't then it creates it. Generate the filtered transactions' PDF with 7 columns: From, To, Memo, Date, Time, Asset Amount, and Fiat Equivalent. --- app/build.gradle | 3 + .../fragments/TransactionsFragment.kt | 14 +- .../bitsybitshareswallet/utils/Constants.kt | 3 + .../utils/PDFGeneratorTask.kt | 132 ++++++++++++++++++ 4 files changed, 151 insertions(+), 1 deletion(-) create mode 100644 app/src/main/java/cy/agorise/bitsybitshareswallet/utils/PDFGeneratorTask.kt diff --git a/app/build.gradle b/app/build.gradle index 4108b52..16973fe 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -103,6 +103,9 @@ dependencies { implementation 'com.google.firebase:firebase-core:16.0.6' implementation 'com.google.firebase:firebase-crash:16.2.1' implementation 'com.crashlytics.sdk.android:crashlytics:2.9.8' + // PDF and CSV generation + implementation 'com.itextpdf:itextpdf:5.5.13' + implementation 'com.opencsv:opencsv:3.7' // Others implementation 'org.bitcoinj:bitcoinj-core:0.14.3' implementation 'com.moldedbits.r2d2:r2d2:1.0.1' diff --git a/app/src/main/java/cy/agorise/bitsybitshareswallet/fragments/TransactionsFragment.kt b/app/src/main/java/cy/agorise/bitsybitshareswallet/fragments/TransactionsFragment.kt index 59660d3..eaae41e 100644 --- a/app/src/main/java/cy/agorise/bitsybitshareswallet/fragments/TransactionsFragment.kt +++ b/app/src/main/java/cy/agorise/bitsybitshareswallet/fragments/TransactionsFragment.kt @@ -4,6 +4,7 @@ import android.Manifest import android.content.pm.PackageManager import android.graphics.Point import android.os.Bundle +import android.os.Environment import android.preference.PreferenceManager import android.view.* import androidx.appcompat.widget.SearchView @@ -21,11 +22,13 @@ import cy.agorise.bitsybitshareswallet.adapters.TransfersDetailsAdapter import cy.agorise.bitsybitshareswallet.database.joins.TransferDetail import cy.agorise.bitsybitshareswallet.utils.BounceTouchListener import cy.agorise.bitsybitshareswallet.utils.Constants +import cy.agorise.bitsybitshareswallet.utils.PDFGeneratorTask import cy.agorise.bitsybitshareswallet.utils.toast import cy.agorise.bitsybitshareswallet.viewmodels.TransferDetailViewModel import io.reactivex.android.schedulers.AndroidSchedulers import io.reactivex.disposables.CompositeDisposable import kotlinx.android.synthetic.main.fragment_transactions.* +import java.io.File import java.util.* import java.util.concurrent.TimeUnit import kotlin.collections.ArrayList @@ -265,8 +268,17 @@ class TransactionsFragment : Fragment(), FilterOptionsDialog.OnFilterOptionsSele } } - /** Created the export procedures for PDF and CSV, depending on the user selection. */ + /** 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 + } + + if (exportPDF) + activity?.let { PDFGeneratorTask(it).execute(filteredTransfersDetails) } } diff --git a/app/src/main/java/cy/agorise/bitsybitshareswallet/utils/Constants.kt b/app/src/main/java/cy/agorise/bitsybitshareswallet/utils/Constants.kt index 592b280..5f93084 100644 --- a/app/src/main/java/cy/agorise/bitsybitshareswallet/utils/Constants.kt +++ b/app/src/main/java/cy/agorise/bitsybitshareswallet/utils/Constants.kt @@ -95,4 +95,7 @@ object Constants { /** Constant used to decide whether or not to update the tellers and merchants info from the webservice */ const val MERCHANTS_UPDATE_PERIOD = 1000L * 60 * 60 + 24 // 1 day + + /** Name of the external storage folder used to save files like PDF and CSV exports and Backups **/ + const val EXTERNAL_STORAGE_FOLDER = "BiTSy" } diff --git a/app/src/main/java/cy/agorise/bitsybitshareswallet/utils/PDFGeneratorTask.kt b/app/src/main/java/cy/agorise/bitsybitshareswallet/utils/PDFGeneratorTask.kt new file mode 100644 index 0000000..457f04f --- /dev/null +++ b/app/src/main/java/cy/agorise/bitsybitshareswallet/utils/PDFGeneratorTask.kt @@ -0,0 +1,132 @@ +package cy.agorise.bitsybitshareswallet.utils + +import android.content.Context +import android.os.AsyncTask +import android.os.Environment +import android.util.Log +import com.itextpdf.text.Document +import com.itextpdf.text.PageSize +import com.itextpdf.text.Paragraph +import com.itextpdf.text.pdf.PdfPCell +import com.itextpdf.text.pdf.PdfPTable +import com.itextpdf.text.pdf.PdfWriter +import cy.agorise.bitsybitshareswallet.R +import cy.agorise.bitsybitshareswallet.database.joins.TransferDetail +import java.io.File +import java.io.FileOutputStream +import java.io.FileWriter +import java.io.IOException +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 mContext: WeakReference = WeakReference(context) + + override fun doInBackground(vararg params: List): String { + return createPDFDocument(params[0]) + } + + private fun createPDFDocument(transferDetails: List): String { + val document = Document(PageSize.A4.rotate()) + return try { + // Create and configure a new PDF file to save the transfers list + val externalStorageFolder = Environment.getExternalStorageDirectory().absolutePath + File.separator + + Constants.EXTERNAL_STORAGE_FOLDER + val fileName = mContext.get()?.resources?.let { + "${it.getString(R.string.app_name)}-${it.getString(R.string.title_transactions)}"} + ".pdf" + val filePath = combinePath(externalStorageFolder, fileName) + createEmptyFile(filePath) + PdfWriter.getInstance(document, FileOutputStream(filePath)) + document.open() + + // Configure pdf table with 8 columns + val table = PdfPTable(7) + + // Add the table header + val columnNames = arrayOf("From", "To", "Memo", "Date", "Time", "Asset Amount", "Fiat Equivalent") + for (columnName in columnNames) { + val cell = PdfPCell(Paragraph(columnName)) + table.addCell(cell) + } + table.completeRow() + + // Configure date and time formats to reuse in all the transfers + val locale = mContext.get()?.resources?.configuration?.locale + val calendar = Calendar.getInstance() + val dateFormat = SimpleDateFormat("MM-dd-yyyy", locale) + val timeFormat = SimpleDateFormat("HH:MM:ss", locale) + + // Save all the transfers information + for ( (index, transferDetail) in transferDetails.withIndex()) { + calendar.timeInMillis = transferDetail.date * 1000 + val date = calendar.time + + table.addCell(makeCell(transferDetail.from ?: "")) // From + table.addCell(makeCell(transferDetail.to ?: "")) // To + table.addCell(makeCell(transferDetail.memo)) // Memo + table.addCell(makeCell(dateFormat.format(date))) // Date + table.addCell(makeCell(timeFormat.format(date))) // Time + + // Asset Amount + val assetPrecision = transferDetail.assetPrecision + val assetAmount = transferDetail.assetAmount.toDouble() / Math.pow(10.0, assetPrecision.toDouble()) + table.addCell(makeCell(String.format("%.${assetPrecision}f %s", assetAmount, transferDetail.assetSymbol))) + + // Fiat Equivalent TODO add once Nelson finishes + + table.completeRow() + + // TODO update progress + } + document.add(table) + document.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 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() + } + + } + + /** Hides the simple but repetitive logic of creating a PDF table cell */ + private fun makeCell(text: String) : PdfPCell { + return PdfPCell(Paragraph(text)) + } + + override fun onProgressUpdate(values: Array) { + // TODO show progress + } + + override fun onPostExecute(message: String) { + mContext.get()?.toast(message) + } +} \ No newline at end of file