Export CSV transactions without requiring storage permission.
This commit is contained in:
parent
830eceeaad
commit
3db6ebb6b0
4 changed files with 102 additions and 114 deletions
|
@ -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")
|
||||||
|
}
|
||||||
|
}
|
|
@ -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)
|
||||||
|
@ -89,7 +90,7 @@ class ExportTransactionsToPdfUseCase(private val applicationContext: Context) {
|
||||||
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",
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
Loading…
Reference in a new issue