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/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 89a2e61..11caa05 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,45 +1,49 @@ + xmlns:tools="http://schemas.android.com/tools" + package="cy.agorise.bitsybitshareswallet"> - - - - - + + + + android:value="@string/google_maps_key" /> - + + - + + - + + @@ -50,8 +54,8 @@ + android:exported="false" + android:grantUriPermissions="true"> diff --git a/app/src/main/java/cy/agorise/bitsybitshareswallet/domain/usecase/ExportTransactionsToCsvUseCase.kt b/app/src/main/java/cy/agorise/bitsybitshareswallet/domain/usecase/ExportTransactionsToCsvUseCase.kt new file mode 100644 index 0000000..02f6a6a --- /dev/null +++ b/app/src/main/java/cy/agorise/bitsybitshareswallet/domain/usecase/ExportTransactionsToCsvUseCase.kt @@ -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, + 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") + } +} 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..68d83d6 --- /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.* +import kotlin.math.pow + +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 + val locale = ConfigurationCompat.getLocales(applicationContext.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.IO) { // 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 / 10.0.pow(assetPrecision) + 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 / 10.0.pow(currency.defaultFractionDigits) + 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/fragments/EReceiptFragment.kt b/app/src/main/java/cy/agorise/bitsybitshareswallet/fragments/EReceiptFragment.kt index eee0541..3bb9b1d 100644 --- a/app/src/main/java/cy/agorise/bitsybitshareswallet/fragments/EReceiptFragment.kt +++ b/app/src/main/java/cy/agorise/bitsybitshareswallet/fragments/EReceiptFragment.kt @@ -1,8 +1,6 @@ package cy.agorise.bitsybitshareswallet.fragments -import android.Manifest import android.content.Intent -import android.content.pm.PackageManager import android.graphics.drawable.Animatable import android.os.Bundle import android.text.method.LinkMovementMethod @@ -20,7 +18,6 @@ import cy.agorise.bitsybitshareswallet.database.joins.TransferDetail import cy.agorise.bitsybitshareswallet.databinding.FragmentEReceiptBinding import cy.agorise.bitsybitshareswallet.utils.Constants import cy.agorise.bitsybitshareswallet.utils.Helper -import cy.agorise.bitsybitshareswallet.utils.toast import cy.agorise.bitsybitshareswallet.viewmodels.EReceiptViewModel import java.math.RoundingMode import java.text.DecimalFormat @@ -28,15 +25,10 @@ import java.text.DecimalFormatSymbols import java.text.NumberFormat import java.text.SimpleDateFormat import java.util.* +import kotlin.math.pow class EReceiptFragment : Fragment() { - companion object { - private const val TAG = "EReceiptFragment" - - private const val REQUEST_WRITE_EXTERNAL_STORAGE_PERMISSION = 100 - } - private val args: EReceiptFragmentArgs by navArgs() private val viewModel: EReceiptViewModel by viewModels() @@ -75,9 +67,9 @@ class EReceiptFragment : Fragment() { val transferId = args.transferId - viewModel.get(userId, transferId).observe(viewLifecycleOwner, { transferDetail -> + viewModel.get(userId, transferId).observe(viewLifecycleOwner) { transferDetail -> bindTransferDetail(transferDetail) - }) + } } private fun bindTransferDetail(transferDetail: TransferDetail) { @@ -95,7 +87,7 @@ class EReceiptFragment : Fragment() { df.decimalFormatSymbols = DecimalFormatSymbols(Locale.getDefault()) val amount = transferDetail.assetAmount.toDouble() / - Math.pow(10.toDouble(), transferDetail.assetPrecision.toDouble()) + 10.toDouble().pow(transferDetail.assetPrecision.toDouble()) val assetAmount = "${df.format(amount)} ${transferDetail.getUIAssetSymbol()}" binding.tvAmount.text = assetAmount @@ -104,7 +96,7 @@ class EReceiptFragment : Fragment() { val numberFormat = NumberFormat.getNumberInstance() val currency = Currency.getInstance(transferDetail.fiatSymbol) val fiatEquivalent = transferDetail.fiatAmount.toDouble() / - Math.pow(10.0, currency.defaultFractionDigits.toDouble()) + 10.0.pow(currency.defaultFractionDigits.toDouble()) val equivalentValue = "${numberFormat.format(fiatEquivalent)} ${currency.currencyCode}" binding.tvEquivalentValue.text = equivalentValue @@ -151,47 +143,13 @@ class EReceiptFragment : Fragment() { override fun onOptionsItemSelected(item: MenuItem): Boolean { if (item.itemId == R.id.menu_share) { - verifyStoragePermission() + shareEReceiptScreenshot() return true } + return super.onOptionsItemSelected(item) } - /** Verifies if the storage permission is already granted, if that is the case then it takes the screenshot and - * shares it but if it is not then it asks the user for that permission */ - 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 - shareEReceiptScreenshot() - } - } - - 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)) { - shareEReceiptScreenshot() - } else { - context?.toast(getString(R.string.msg__storage_permission_necessary_share)) - } - return - } - } - /** Takes a screenshot as a bitmap (hiding the tx hyperlink), saves it into a temporal cache image and then * sends an intent so the user can select the desired method to share the image. */ private fun shareEReceiptScreenshot() { @@ -214,4 +172,8 @@ class EReceiptFragment : Fragment() { shareIntent.type = "*/*" startActivity(Intent.createChooser(shareIntent, getString(R.string.text__share_with))) } -} \ No newline at end of file + + companion object { + private const val TAG = "EReceiptFragment" + } +} diff --git a/app/src/main/java/cy/agorise/bitsybitshareswallet/fragments/ReceiveTransactionFragment.kt b/app/src/main/java/cy/agorise/bitsybitshareswallet/fragments/ReceiveTransactionFragment.kt index 8878238..0b73d1e 100644 --- a/app/src/main/java/cy/agorise/bitsybitshareswallet/fragments/ReceiveTransactionFragment.kt +++ b/app/src/main/java/cy/agorise/bitsybitshareswallet/fragments/ReceiveTransactionFragment.kt @@ -1,7 +1,6 @@ package cy.agorise.bitsybitshareswallet.fragments import android.content.Intent -import android.content.pm.PackageManager import android.graphics.drawable.Animatable import android.os.Bundle import android.util.Log @@ -22,7 +21,6 @@ import cy.agorise.bitsybitshareswallet.databinding.FragmentReceiveTransactionBin import cy.agorise.bitsybitshareswallet.utils.Constants import cy.agorise.bitsybitshareswallet.utils.Helper import cy.agorise.bitsybitshareswallet.utils.showKeyboard -import cy.agorise.bitsybitshareswallet.utils.toast import cy.agorise.bitsybitshareswallet.viewmodels.ReceiveTransactionViewModel import cy.agorise.graphenej.* import cy.agorise.graphenej.api.ConnectionStatusUpdate @@ -34,23 +32,10 @@ import java.text.DecimalFormat import java.text.DecimalFormatSymbols import java.util.* import java.util.concurrent.TimeUnit -import kotlin.collections.ArrayList import kotlin.math.min class ReceiveTransactionFragment : ConnectedFragment() { - companion object { - private const val TAG = "ReceiveTransactionFrag" - - private const val RESPONSE_LIST_ASSETS = 1 - private const val REQUEST_WRITE_EXTERNAL_STORAGE_PERMISSION = 100 - - /** Number of assets to request from the NetworkService to show as suggestions in the AutoCompleteTextView */ - private const val AUTO_SUGGEST_ASSET_LIMIT = 5 - - private const val OTHER_ASSET = "other_asset" - } - private val viewModel: ReceiveTransactionViewModel by viewModels() private var _binding: FragmentReceiveTransactionBinding? = null @@ -120,11 +105,11 @@ class ReceiveTransactionFragment : ConnectedFragment() { val userId = PreferenceManager.getDefaultSharedPreferences(context) .getString(Constants.KEY_CURRENT_ACCOUNT_ID, "") - viewModel.getUserAccount(userId!!).observe(viewLifecycleOwner, { user -> + viewModel.getUserAccount(userId!!).observe(viewLifecycleOwner) { user -> mUserAccount = UserAccount(user.id, user.name) - }) + } - viewModel.getAllNonZero().observe(viewLifecycleOwner, { assets -> + viewModel.getAllNonZero().observe(viewLifecycleOwner) { assets -> mAssets.clear() mAssets.addAll(assets) @@ -155,11 +140,11 @@ class ReceiveTransactionFragment : ConnectedFragment() { break } } - }) + } - viewModel.qrCodeBitmap.observe(viewLifecycleOwner, { bitmap -> + viewModel.qrCodeBitmap.observe(viewLifecycleOwner) { bitmap -> binding.ivQR.setImageBitmap(bitmap) - }) + } binding.spAsset.onItemSelectedListener = object : AdapterView.OnItemSelectedListener { override fun onNothingSelected(parent: AdapterView<*>?) {} @@ -351,47 +336,12 @@ class ReceiveTransactionFragment : ConnectedFragment() { override fun onOptionsItemSelected(item: MenuItem): Boolean { if (item.itemId == R.id.menu_share) { - verifyStoragePermission() + shareQRScreenshot() return true } return super.onOptionsItemSelected(item) } - private fun verifyStoragePermission() { - if (ContextCompat.checkSelfPermission( - requireActivity(), - android.Manifest.permission.WRITE_EXTERNAL_STORAGE - ) - != PackageManager.PERMISSION_GRANTED - ) { - // Permission is not already granted - requestPermissions( - arrayOf(android.Manifest.permission.WRITE_EXTERNAL_STORAGE), - REQUEST_WRITE_EXTERNAL_STORAGE_PERMISSION - ) - } else { - // Permission is already granted - shareQRScreenshot() - } - } - - 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)) { - shareQRScreenshot() - } else { - context?.toast(getString(R.string.msg__storage_permission_necessary_share)) - } - return - } - } - /** * This function takes a screenshot as a bitmap, saves it into a temporal cache image and then * sends an intent so the user can select the desired method to share the image. @@ -421,4 +371,15 @@ class ReceiveTransactionFragment : ConnectedFragment() { shareIntent.type = "*/*" startActivity(Intent.createChooser(shareIntent, getString(R.string.text__share_with))) } + + companion object { + private const val TAG = "ReceiveTransactionFrag" + + private const val RESPONSE_LIST_ASSETS = 1 + + /** Number of assets to request from the NetworkService to show as suggestions in the AutoCompleteTextView */ + private const val AUTO_SUGGEST_ASSET_LIMIT = 5 + + private const val OTHER_ASSET = "other_asset" + } } 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 65% 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..8b90a29 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 @@ -18,14 +15,11 @@ import com.afollestad.materialdialogs.list.listItemsMultiChoice import com.google.firebase.crashlytics.FirebaseCrashlytics 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 +29,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 +69,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 +85,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 +127,7 @@ class TransactionsFragment : Fragment(), FilterOptionsDialog.OnFilterOptionsSele true } R.id.menu_export -> { - verifyStoragePermission() + showExportOptionsDialog() true } else -> super.onOptionsItemSelected(item) @@ -162,75 +153,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 +182,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/ui/transactions/TransactionsViewModel.kt b/app/src/main/java/cy/agorise/bitsybitshareswallet/ui/transactions/TransactionsViewModel.kt new file mode 100644 index 0000000..3dc48e9 --- /dev/null +++ b/app/src/main/java/cy/agorise/bitsybitshareswallet/ui/transactions/TransactionsViewModel.kt @@ -0,0 +1,173 @@ +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.domain.usecase.ExportTransactionsToCsvUseCase +import cy.agorise.bitsybitshareswallet.domain.usecase.ExportTransactionsToPdfUseCase +import cy.agorise.bitsybitshareswallet.repositories.TransferDetailRepository +import cy.agorise.bitsybitshareswallet.utils.Helper +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import java.util.* + + +class TransactionsViewModel(application: Application) : AndroidViewModel(application) { + + // TODO Inject below dependencies + private val exportTransactionsToPdfUseCase = ExportTransactionsToPdfUseCase(application) + private val exportTransactionsToCsvUseCase = ExportTransactionsToCsvUseCase(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> + + /** + * This [MediatorLiveData] is used to combine two sources of information into one, keeping the + * client of this [ViewModel] receiving only one stream of data (a list of filtered [TransferDetail]) + */ + private val filteredTransactions = MediatorLiveData>() + + init { + // Initialize the start and end dates for the FilterOptions + val calendar = getClearedUtc() + calendar.timeInMillis = MaterialDatePicker.todayInUtcMilliseconds() + mFilterOptions.endDate = calendar.timeInMillis + calendar.roll(Calendar.MONTH, -2) + mFilterOptions.startDate = calendar.timeInMillis + } + + private fun getClearedUtc(): Calendar { + val utc = Calendar.getInstance(TimeZone.getTimeZone("UTC")) + utc.clear() + return utc + } + + internal fun getFilteredTransactions(userId: String): LiveData> { + val currencyCode = Helper.getCoingeckoSupportedCurrency(Locale.getDefault()) + transactions = mRepository.getAll(userId, currencyCode) + + filteredTransactions.addSource(transactions) { transactions -> + viewModelScope.launch { + filteredTransactions.value = filter(transactions, mFilterOptions) + } + } + + return filteredTransactions + } + + internal fun getFilterOptions(): FilterOptions { + return mFilterOptions + } + + 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 + viewModelScope.launch { + filteredTransactions.value = filter(transactions, mFilterOptions) + } + } + + /** + * 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 = withContext(Dispatchers.Default) { + + // Create a list to store the filtered transactions + val filteredTransactions = ArrayList() + + // Make sure the filter dates use the same format as the transactions' dates + val startDate = filterOptions.startDate / 1000 + val endDate = filterOptions.endDate / 1000 + + for (transaction in transactions) { + // Filter by transfer direction + if (transaction.direction) { // Transfer sent + if (filterOptions.transactionsDirection == 1) + // Looking for received transfers only + continue + } else { // Transfer received + if (filterOptions.transactionsDirection == 2) + // Looking for sent transactions only + continue + } + + // Filter by date range + if (!filterOptions.dateRangeAll && (transaction.date < startDate || + transaction.date > endDate) + ) + continue + + // Filter by asset + if (!filterOptions.assetAll && transaction.assetSymbol != filterOptions.asset) + continue + + // Filter by equivalent value + if (!filterOptions.equivalentValueAll && ((transaction.fiatAmount + ?: -1) < filterOptions.fromEquivalentValue + || (transaction.fiatAmount ?: -1) > filterOptions.toEquivalentValue) + ) + continue + + // Filter transactions sent to agorise + if (filterOptions.agoriseFees && transaction.to.equals("agorise")) + continue + + // Filter by search query + val text = "${transaction.from ?: ""} ${transaction.to ?: ""} ${transaction.memo}" + if (text.contains(filterOptions.query, ignoreCase = true)) { + filteredTransactions.add(transaction) + } + } + + filteredTransactions + } + + /** 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 + val transactions = filteredTransactions.value ?: return + + if (exportPdf) { + viewModelScope.launch { + // TODO Show success/failure message + exportTransactionsToPdfUseCase(transactions, folderDocumentFile) + } + } + + if (exportCsv) { + viewModelScope.launch { + // TODO Show success/failure message + exportTransactionsToCsvUseCase(transactions, folderDocumentFile) + } + } + } + + companion object { + private const val TAG = "TransactionsViewModel" + } +} diff --git a/app/src/main/java/cy/agorise/bitsybitshareswallet/adapters/TransfersDetailsAdapter.kt b/app/src/main/java/cy/agorise/bitsybitshareswallet/ui/transactions/TransfersDetailsAdapter.kt similarity index 97% rename from app/src/main/java/cy/agorise/bitsybitshareswallet/adapters/TransfersDetailsAdapter.kt rename to app/src/main/java/cy/agorise/bitsybitshareswallet/ui/transactions/TransfersDetailsAdapter.kt index 93f26d5..6473b21 100644 --- a/app/src/main/java/cy/agorise/bitsybitshareswallet/adapters/TransfersDetailsAdapter.kt +++ b/app/src/main/java/cy/agorise/bitsybitshareswallet/ui/transactions/TransfersDetailsAdapter.kt @@ -1,4 +1,4 @@ -package cy.agorise.bitsybitshareswallet.adapters +package cy.agorise.bitsybitshareswallet.ui.transactions import android.content.Context import android.util.Log @@ -16,7 +16,6 @@ import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.SortedList import cy.agorise.bitsybitshareswallet.R import cy.agorise.bitsybitshareswallet.database.joins.TransferDetail -import cy.agorise.bitsybitshareswallet.fragments.TransactionsFragmentDirections import java.math.RoundingMode import java.text.DecimalFormat import java.text.DecimalFormatSymbols @@ -93,7 +92,7 @@ class TransfersDetailsAdapter(private val context: Context) : val tvFiatEquivalent: TextView = itemView.findViewById(R.id.tvFiatEquivalent) } - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TransfersDetailsAdapter.ViewHolder { + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { val inflater = LayoutInflater.from(context) val transactionView = inflater.inflate(R.layout.item_transaction, parent, false) diff --git a/app/src/main/java/cy/agorise/bitsybitshareswallet/utils/CSVGenerationTask.kt b/app/src/main/java/cy/agorise/bitsybitshareswallet/utils/CSVGenerationTask.kt deleted file mode 100644 index 75fb185..0000000 --- a/app/src/main/java/cy/agorise/bitsybitshareswallet/utils/CSVGenerationTask.kt +++ /dev/null @@ -1,102 +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, Int, String>() { - - companion object { - private const val TAG = "CSVGenerationTask" - } - - private val mContext: WeakReference = WeakReference(context) - - override fun doInBackground(vararg params: List): String { - return createCSVDocument(params[0]) - } - - private fun createCSVDocument(transferDetails: List): String { - return try { - // Create and configure a new CSV 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)}"} + ".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) { - // TODO show progress - } - - override fun onPostExecute(message: String) { - mContext.get()?.toast(message) - } -} \ No newline at end of file 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 0ad9922..b56aa92 100644 --- a/app/src/main/java/cy/agorise/bitsybitshareswallet/utils/Constants.kt +++ b/app/src/main/java/cy/agorise/bitsybitshareswallet/utils/Constants.kt @@ -48,9 +48,6 @@ object Constants { /** Coingecko's API URL */ const val COINGECKO_URL = "https://api.coingecko.com" - /** Key used to store and retrieve a cache of the coingecko supported currencies */ - const val KEY_COINGECKO_CURRENCIES_CACHE = "key_coingecko_currencies_cache" - /** The fee to send in every transfer (0.01%) */ const val FEE_PERCENTAGE = 0.0001 @@ -122,9 +119,6 @@ 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 + 3 // 3 hours - /** Name of the external storage folder used to save files like PDF and CSV exports and Backups **/ - const val EXTERNAL_STORAGE_FOLDER = "BiTSy" - /** Constant used to check if the current connected node is out of sync */ const val CHECK_NODE_OUT_OF_SYNC = 10 // 10 seconds 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/java/cy/agorise/bitsybitshareswallet/viewmodels/TransactionsViewModel.kt b/app/src/main/java/cy/agorise/bitsybitshareswallet/viewmodels/TransactionsViewModel.kt deleted file mode 100644 index 7a1f952..0000000 --- a/app/src/main/java/cy/agorise/bitsybitshareswallet/viewmodels/TransactionsViewModel.kt +++ /dev/null @@ -1,136 +0,0 @@ -package cy.agorise.bitsybitshareswallet.viewmodels - -import android.app.Application -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.repositories.TransferDetailRepository -import cy.agorise.bitsybitshareswallet.utils.Helper -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext -import java.util.* - - -class TransactionsViewModel(application: Application) : AndroidViewModel(application) { - companion object { - const val TAG = "TransactionsViewModel" - } - private var mRepository = TransferDetailRepository(application) - - /** - * [FilterOptions] used to filter the list of [TransferDetail] taken from the database - */ - private var mFilterOptions = FilterOptions() - - private lateinit var transactions : LiveData> - - /** - * This [MediatorLiveData] is used to combine two sources of information into one, keeping the - * client of this [ViewModel] receiving only one stream of data (a list of filtered [TransferDetail]) - */ - private val filteredTransactions = MediatorLiveData>() - - init { - // Initialize the start and end dates for the FilterOptions - val calendar = getClearedUtc() - calendar.timeInMillis = MaterialDatePicker.todayInUtcMilliseconds() - mFilterOptions.endDate = calendar.timeInMillis - calendar.roll(Calendar.MONTH, -2) - mFilterOptions.startDate = calendar.timeInMillis - } - - private fun getClearedUtc(): Calendar { - val utc = Calendar.getInstance(TimeZone.getTimeZone("UTC")) - utc.clear() - return utc - } - - internal fun getFilteredTransactions(userId: String): LiveData> { - val currencyCode = Helper.getCoingeckoSupportedCurrency(Locale.getDefault()) - transactions = mRepository.getAll(userId, currencyCode) - - filteredTransactions.addSource(transactions) { transactions -> - viewModelScope.launch { - filteredTransactions.value = filter(transactions, mFilterOptions) - } - } - - return filteredTransactions - } - - internal fun getFilterOptions(): FilterOptions { - return mFilterOptions - } - - 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 - viewModelScope.launch { - filteredTransactions.value = filter(transactions, mFilterOptions) - } - } - - internal fun getFilteredTransactionsOnce() = filteredTransactions.value - - /** - * 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 { - return withContext(Dispatchers.Default) { - - // Create a list to store the filtered transactions - val filteredTransactions = ArrayList() - - // Make sure the filter dates use the same format as the transactions' dates - val startDate = filterOptions.startDate / 1000 - val endDate = filterOptions.endDate / 1000 - - for (transaction in transactions) { - // Filter by transfer direction - if (transaction.direction) { // Transfer sent - if (filterOptions.transactionsDirection == 1) - // Looking for received transfers only - continue - } else { // Transfer received - if (filterOptions.transactionsDirection == 2) - // Looking for sent transactions only - continue - } - - // Filter by date range - if (!filterOptions.dateRangeAll && (transaction.date < startDate || - transaction.date > endDate)) - continue - - // Filter by asset - if (!filterOptions.assetAll && transaction.assetSymbol != filterOptions.asset) - continue - - // Filter by equivalent value - if (!filterOptions.equivalentValueAll && ((transaction.fiatAmount ?: -1 ) < filterOptions.fromEquivalentValue - || (transaction.fiatAmount ?: -1) > filterOptions.toEquivalentValue)) - continue - - // Filter transactions sent to agorise - if (filterOptions.agoriseFees && transaction.to.equals("agorise")) - continue - - // Filter by search query - val text = "${transaction.from ?: ""} ${transaction.to ?: ""} ${transaction.memo}" - if (text.contains(filterOptions.query, ignoreCase = true)) { - filteredTransactions.add(transaction) - } - } - - filteredTransactions - } - } -} \ 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..8dfe5c0 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 @@ -126,7 +125,6 @@ BiTSy Rechnung von %1$s Aktie Teilen mit - Für die Freigabe von Bildern ist eine Speichererlaubnis erforderlich. Einstellungen diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index f5a0286..8c1178d 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 @@ -125,7 +124,6 @@ Invoice BiTSy de %1$s Compartir Compartir con - El permiso de almacenamiento es necesario para compartir imágenes. Ajustes diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 1493c4d..56bf69e 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 @@ -126,7 +125,6 @@ Facture biTSy de %1$s Partager Partager avec - Une autorisation de stockage est nécessaire pour partager des images. Réglages diff --git a/app/src/main/res/values-hi/strings.xml b/app/src/main/res/values-hi/strings.xml index eeeb6a1..23eac81 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 फ़ाइलों को निर्यात करने के लिए भंडारण अनुमति आवश्यक है। से सेवा मेरे मेमो @@ -126,7 +125,6 @@ %1$s से बीटीएसआई चालान शेयर के साथ शेयर करें - छवियों को साझा करने के लिए भंडारण की अनुमति आवश्यक है। सेटिंग्स diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index 88f9a58..faaa736 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ファイルをエクスポートするには、ストレージ許可が必要です。 から メモ @@ -126,7 +125,6 @@ %1$sからのBiTSy請求書 シェア と共有する - 画像を共有するには保存許可が必要です。 設定 diff --git a/app/src/main/res/values-pa/strings.xml b/app/src/main/res/values-pa/strings.xml index 8119bb9..4a50ad2 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 ਫਾਈਲਾਂ ਨੂੰ ਨਿਰਯਾਤ ਕਰਨ ਲਈ ਜ਼ਰੂਰੀ ਹੈ. ਤੋਂ ਨੂੰ ਮੀਮੋ @@ -126,7 +125,6 @@ %1$s ਦਾ ਬੀ.ਟੀ.ਸੀ ਚਲਾਨ ਸਾਂਝਾ ਕਰੋ ਨਾਲ ਸਾਂਝਾ ਕਰੋ - ਤਸਵੀਰਾਂ ਨੂੰ ਸਾਂਝਾ ਕਰਨ ਲਈ ਸਟੋਰੇਜ਼ ਦੀ ਆਗਿਆ ਜ਼ਰੂਰੀ ਹੈ. ਸੈਟਿੰਗਜ਼ diff --git a/app/src/main/res/values-pt/strings.xml b/app/src/main/res/values-pt/strings.xml index 57520ef..66171fe 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 @@ -126,7 +125,6 @@ Fatura BiTSy de %1$s Compartilhar Compartilhar com - É necessária permissão de armazenamento para compartilhar imagens. Definições diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index e2c93d5..643c9b8 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. От к напоминание @@ -126,7 +125,6 @@ Счет BiTSy от %1$s Поделиться Поделиться с - Разрешение на хранение необходимо для обмена изображениями. настройки diff --git a/app/src/main/res/values-zh/strings.xml b/app/src/main/res/values-zh/strings.xml index 3d645e3..9f930ab 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文件需要存储权限。 备忘录 @@ -126,7 +125,6 @@ 来自%1$s的BiTSy发票 分享 与某人分享 - 共享图像需要存储权限。 设置 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 0109835..7e11008 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 @@ -128,7 +127,6 @@ BiTSy invoice from %1$s Share Share with - Storage permission is necessary to share images. Settings