Export CSV transactions without requiring storage permission.

This commit is contained in:
Severiano Jaramillo 2022-05-28 11:20:07 -07:00
parent 830eceeaad
commit 3db6ebb6b0
4 changed files with 102 additions and 114 deletions

View file

@ -0,0 +1,86 @@
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.opencsv.CSVWriter
import cy.agorise.bitsybitshareswallet.R
import cy.agorise.bitsybitshareswallet.database.joins.TransferDetail
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.io.OutputStreamWriter
import java.text.SimpleDateFormat
import java.util.*
import kotlin.math.pow
class ExportTransactionsToCsvUseCase(private val applicationContext: Context) {
suspend operator fun invoke(
transactions: List<TransferDetail>,
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)}.csv"
}
// Obtains the path to store the PDF
val csvDocUri = folderDocumentFile.createFile("text/csv", fileName)?.uri
val contentResolver = applicationContext.contentResolver
val csvOutputStream = contentResolver.openOutputStream(csvDocUri!!, "w")
val csvWriter = CSVWriter(OutputStreamWriter(csvOutputStream))
// Add the table header
csvWriter.writeNext(
arrayOf(
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
).map { columnNameId -> applicationContext.getString(columnNameId) }.toTypedArray()
)
// Configure date and time formats to reuse in all the transfers
val locale = ConfigurationCompat.getLocales(applicationContext.resources.configuration)[0]
val dateFormat = SimpleDateFormat("MM-dd-yyyy", locale)
val timeFormat = SimpleDateFormat("HH:mm:ss", locale)
// Save all the transfers information
val row = Array(7) { "" } // Array initialized with empty strings
for ((index, transferDetail) in transactions.withIndex()) {
val date = Date(transferDetail.date * 1000)
row[0] = transferDetail.from ?: "" // From
row[1] = transferDetail.to ?: "" // To
row[2] = transferDetail.memo // Memo
row[3] = dateFormat.format(date) // Date
row[4] = timeFormat.format(date) // Time
// Asset Amount
val assetPrecision = transferDetail.assetPrecision
val assetAmount = transferDetail.assetAmount / 10.0.pow(assetPrecision)
row[5] =
String.format("%.${assetPrecision}f %s", assetAmount, transferDetail.assetSymbol)
// Fiat Equivalent
row[6] = if (transferDetail.fiatAmount != null && transferDetail.fiatSymbol != null) {
val currency = Currency.getInstance(transferDetail.fiatSymbol)
val fiatAmount = transferDetail.fiatAmount / 10.0.pow(currency.defaultFractionDigits)
String.format(
"%.${currency.defaultFractionDigits}f %s",
fiatAmount,
currency.currencyCode
)
} else {
""
}
csvWriter.writeNext(row)
// TODO update progress
}
csvWriter.close()
Log.d("ExportTransactionsToCsv", "CSV generated and saved")
}
}

View file

@ -12,6 +12,7 @@ import kotlinx.coroutines.withContext
import java.io.BufferedOutputStream import java.io.BufferedOutputStream
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.* import java.util.*
import kotlin.math.pow
class ExportTransactionsToPdfUseCase(private val applicationContext: Context) { class ExportTransactionsToPdfUseCase(private val applicationContext: Context) {
@ -45,8 +46,10 @@ class ExportTransactionsToPdfUseCase(private val applicationContext: Context) {
val tableData = mutableListOf<List<Cell>>() val tableData = mutableListOf<List<Cell>>()
// Add column names/headers // Add column names/headers
val columnNames = intArrayOf(R.string.title_from, R.string.title_to, R.string.title_memo, R.string.title_date, val columnNames = intArrayOf(
R.string.title_time, R.string.title_amount, R.string.title_equivalent_value) 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 -> val header = columnNames.map { columnName ->
Cell(f1, applicationContext.getString(columnName)).apply { setPadding(2f) } Cell(f1, applicationContext.getString(columnName)).apply { setPadding(2f) }
@ -56,10 +59,8 @@ class ExportTransactionsToPdfUseCase(private val applicationContext: Context) {
tableData.add(header) tableData.add(header)
// Add the table contents // Add the table contents
applicationContext.let { context -> val locale = ConfigurationCompat.getLocales(applicationContext.resources.configuration)[0]
val locale = ConfigurationCompat.getLocales(context.resources.configuration)[0] tableData.addAll(getData(transactions, f2, locale))
tableData.addAll(getData(transactions, f2, locale))
}
// Configure the PDF table // Configure the PDF table
table.setData(tableData, Table.DATA_HAS_1_HEADER_ROWS) table.setData(tableData, Table.DATA_HAS_1_HEADER_ROWS)
@ -82,14 +83,14 @@ class ExportTransactionsToPdfUseCase(private val applicationContext: Context) {
pdf.close() pdf.close()
Log.d("ExportTransactionsToPdf","PDF generated and saved") Log.d("ExportTransactionsToPdf", "PDF generated and saved")
} }
private suspend fun getData( private suspend fun getData(
transferDetails: List<TransferDetail>, transferDetails: List<TransferDetail>,
font: Font, font: Font,
locale: Locale locale: Locale
): List<List<Cell>> = withContext(Dispatchers.Default) { // TODO Inject Dispatcher ): List<List<Cell>> = withContext(Dispatchers.IO) { // TODO Inject Dispatcher
val tableData = mutableListOf<List<Cell>>() val tableData = mutableListOf<List<Cell>>()
@ -112,8 +113,7 @@ class ExportTransactionsToPdfUseCase(private val applicationContext: Context) {
// Asset Amount // Asset Amount
val assetPrecision = transferDetail.assetPrecision val assetPrecision = transferDetail.assetPrecision
val assetAmount = val assetAmount = transferDetail.assetAmount / 10.0.pow(assetPrecision)
transferDetail.assetAmount.toDouble() / Math.pow(10.0, assetPrecision.toDouble())
cols.add( cols.add(
String.format( String.format(
"%.${assetPrecision}f %s", "%.${assetPrecision}f %s",
@ -125,8 +125,8 @@ class ExportTransactionsToPdfUseCase(private val applicationContext: Context) {
// Fiat Equivalent // Fiat Equivalent
if (transferDetail.fiatAmount != null && transferDetail.fiatSymbol != null) { if (transferDetail.fiatAmount != null && transferDetail.fiatSymbol != null) {
val currency = Currency.getInstance(transferDetail.fiatSymbol) val currency = Currency.getInstance(transferDetail.fiatSymbol)
val fiatAmount = transferDetail.fiatAmount.toDouble() / val fiatAmount =
Math.pow(10.0, currency.defaultFractionDigits.toDouble()) transferDetail.fiatAmount / 10.0.pow(currency.defaultFractionDigits)
cols.add( cols.add(
String.format( String.format(
"%.${currency.defaultFractionDigits}f %s", "%.${currency.defaultFractionDigits}f %s",

View file

@ -7,6 +7,7 @@ import androidx.documentfile.provider.DocumentFile
import androidx.lifecycle.* import androidx.lifecycle.*
import com.google.android.material.datepicker.MaterialDatePicker import com.google.android.material.datepicker.MaterialDatePicker
import cy.agorise.bitsybitshareswallet.database.joins.TransferDetail import cy.agorise.bitsybitshareswallet.database.joins.TransferDetail
import cy.agorise.bitsybitshareswallet.domain.usecase.ExportTransactionsToCsvUseCase
import cy.agorise.bitsybitshareswallet.domain.usecase.ExportTransactionsToPdfUseCase import cy.agorise.bitsybitshareswallet.domain.usecase.ExportTransactionsToPdfUseCase
import cy.agorise.bitsybitshareswallet.repositories.TransferDetailRepository import cy.agorise.bitsybitshareswallet.repositories.TransferDetailRepository
import cy.agorise.bitsybitshareswallet.utils.Helper import cy.agorise.bitsybitshareswallet.utils.Helper
@ -20,6 +21,7 @@ class TransactionsViewModel(application: Application) : AndroidViewModel(applica
// TODO Inject below dependencies // TODO Inject below dependencies
private val exportTransactionsToPdfUseCase = ExportTransactionsToPdfUseCase(application) private val exportTransactionsToPdfUseCase = ExportTransactionsToPdfUseCase(application)
private val exportTransactionsToCsvUseCase = ExportTransactionsToCsvUseCase(application)
private val mRepository = TransferDetailRepository(application) private val mRepository = TransferDetailRepository(application)
/** /**
@ -159,7 +161,8 @@ class TransactionsViewModel(application: Application) : AndroidViewModel(applica
if (exportCsv) { if (exportCsv) {
viewModelScope.launch { viewModelScope.launch {
// TODO Export CSV // TODO Show success/failure message
exportTransactionsToCsvUseCase(transactions, folderDocumentFile)
} }
} }
} }

View file

@ -1,101 +0,0 @@
package cy.agorise.bitsybitshareswallet.utils
import android.content.Context
import android.os.AsyncTask
import android.os.Environment
import android.util.Log
import com.opencsv.CSVWriter
import cy.agorise.bitsybitshareswallet.R
import cy.agorise.bitsybitshareswallet.database.joins.TransferDetail
import java.io.File
import java.io.FileWriter
import java.lang.ref.WeakReference
import java.text.SimpleDateFormat
import java.util.*
/**
* AsyncTask subclass used to move the CSV generation procedure to a background thread
* and inform the UI of the progress.
*/
class CSVGenerationTask(context: Context) : AsyncTask<List<TransferDetail>, Int, String>() {
companion object {
private const val TAG = "CSVGenerationTask"
}
private val mContext: WeakReference<Context> = WeakReference(context)
override fun doInBackground(vararg params: List<TransferDetail>): String {
return createCSVDocument(params[0])
}
private fun createCSVDocument(transferDetails: List<TransferDetail>): String {
return try {
// Create and configure a new CSV file to save the transfers list
val externalStorageFolder = Environment.getExternalStorageDirectory().absolutePath + File.separator
val fileName = mContext.get()?.resources?.let {
"${it.getString(R.string.app_name)}-${it.getString(R.string.title_transactions)}"} + ".csv"
val file = File(externalStorageFolder, fileName)
file.createNewFile()
val csvWriter = CSVWriter(FileWriter(file))
// Add the table header
val row = Array(7) {""} // Array initialized with empty strings
val columnNames = arrayOf(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)
for ((i, columnName) in columnNames.withIndex()) {
row[i] = mContext.get()?.getString(columnName) ?: ""
}
csvWriter.writeNext(row)
// Configure date and time formats to reuse in all the transfers
val locale = mContext.get()?.resources?.configuration?.locale
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()) {
val date = Date(transferDetail.date * 1000)
row[0] = transferDetail.from ?: "" // From
row[1] = transferDetail.to ?: "" // To
row[2] = transferDetail.memo // Memo
row[3] = dateFormat.format(date) // Date
row[4] = timeFormat.format(date) // Time
// Asset Amount
val assetPrecision = transferDetail.assetPrecision
val assetAmount = transferDetail.assetAmount.toDouble() / Math.pow(10.0, assetPrecision.toDouble())
row[5] = String.format("%.${assetPrecision}f %s", assetAmount, transferDetail.assetSymbol)
// Fiat Equivalent
row[6] = 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())
String.format("%.${currency.defaultFractionDigits}f %s", fiatAmount, currency.currencyCode)
} else {
""
}
csvWriter.writeNext(row)
// TODO update progress
}
csvWriter.close()
"CSV generated and saved: ${file.absolutePath}"
} catch (e: Exception) {
Log.e(TAG, "Exception while trying to generate a CSV. Msg: " + e.message)
"Unable to generate CSV. Please retry. Error: ${e.message}"
}
}
override fun onProgressUpdate(values: Array<Int>) {
// TODO show progress
}
override fun onPostExecute(message: String) {
mContext.get()?.toast(message)
}
}