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