Compare commits
No commits in common. "ec54ed0620b02e0db25bbe0e6d3f1b12e1c5faa0" and "96faf9bffd347461a036c7c32ecc70a7224d6188" have entirely different histories.
ec54ed0620
...
96faf9bffd
35 changed files with 722 additions and 544 deletions
|
@ -7,12 +7,11 @@ apply plugin: 'com.google.gms.google-services'
|
|||
apply plugin: 'com.google.firebase.crashlytics'
|
||||
|
||||
android {
|
||||
compileSdk 30
|
||||
|
||||
compileSdkVersion 29
|
||||
defaultConfig {
|
||||
applicationId "cy.agorise.bitsybitshareswallet"
|
||||
minSdk 21
|
||||
targetSdk 30
|
||||
minSdkVersion 21
|
||||
targetSdkVersion 29
|
||||
versionCode 15
|
||||
versionName "0.17.2-beta"
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
|
@ -56,6 +55,10 @@ android {
|
|||
kotlinOptions {
|
||||
jvmTarget = "1.8"
|
||||
}
|
||||
// Gradle automatically adds 'android.test.runner' as a dependency.
|
||||
useLibrary 'android.test.runner'
|
||||
useLibrary 'android.test.base'
|
||||
useLibrary 'android.test.mock'
|
||||
}
|
||||
|
||||
dependencies {
|
||||
|
@ -71,10 +74,8 @@ 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'
|
||||
|
|
|
@ -1,49 +1,45 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
package="cy.agorise.bitsybitshareswallet">
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
package="cy.agorise.bitsybitshareswallet">
|
||||
|
||||
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.CAMERA" />
|
||||
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
<uses-permission android:name="android.permission.CAMERA"/>
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
|
||||
|
||||
<application
|
||||
android:name=".utils.BitsyApplication"
|
||||
android:allowBackup="true"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:label="@string/app_name"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/Theme.Bitsy"
|
||||
android:requestLegacyExternalStorage="true"
|
||||
tools:ignore="GoogleAppIndexingWarning">
|
||||
|
||||
<meta-data
|
||||
android:name="com.google.android.geo.API_KEY"
|
||||
android:value="@string/google_maps_key" />
|
||||
android:value="@string/google_maps_key"/>
|
||||
<!-- Avoid Crashlytics crash collection for all users/builds -->
|
||||
<meta-data
|
||||
android:name="firebase_crashlytics_collection_enabled"
|
||||
android:value="false" />
|
||||
<!-- Avoid crashes with Google maps in SDK 28 (Android 9 [Pie]) -->
|
||||
<uses-library
|
||||
android:name="org.apache.http.legacy"
|
||||
android:required="false" />
|
||||
|
||||
<uses-library android:name="org.apache.http.legacy" android:required="false"/>
|
||||
<activity
|
||||
android:name=".activities.SplashActivity"
|
||||
android:exported="true"
|
||||
android:theme="@style/SplashTheme">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
|
||||
<action android:name="android.intent.action.MAIN"/>
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
<category android:name="android.intent.category.LAUNCHER"/>
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
android:name=".activities.MainActivity"
|
||||
android:exported="false"
|
||||
android:screenOrientation="portrait"
|
||||
android:theme="@style/Theme.Bitsy"
|
||||
android:windowSoftInputMode="adjustPan">
|
||||
|
@ -54,8 +50,8 @@
|
|||
<provider
|
||||
android:name="androidx.core.content.FileProvider"
|
||||
android:authorities="cy.agorise.bitsybitshareswallet.FileProvider"
|
||||
android:exported="false"
|
||||
android:grantUriPermissions="true">
|
||||
android:grantUriPermissions="true"
|
||||
android:exported="false">
|
||||
<meta-data
|
||||
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||
android:resource="@xml/tmp_image_path" />
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
package cy.agorise.bitsybitshareswallet.ui.transactions
|
||||
package cy.agorise.bitsybitshareswallet.adapters
|
||||
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
|
@ -16,6 +16,7 @@ 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
|
||||
|
@ -92,7 +93,7 @@ class TransfersDetailsAdapter(private val context: Context) :
|
|||
val tvFiatEquivalent: TextView = itemView.findViewById(R.id.tvFiatEquivalent)
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TransfersDetailsAdapter.ViewHolder {
|
||||
val inflater = LayoutInflater.from(context)
|
||||
|
||||
val transactionView = inflater.inflate(R.layout.item_transaction, parent, false)
|
|
@ -1,86 +0,0 @@
|
|||
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")
|
||||
}
|
||||
}
|
|
@ -1,166 +0,0 @@
|
|||
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<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)}.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<List<Cell>>()
|
||||
|
||||
// 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<TransferDetail>,
|
||||
font: Font,
|
||||
locale: Locale
|
||||
): List<List<Cell>> = withContext(Dispatchers.IO) { // TODO Inject Dispatcher
|
||||
|
||||
val tableData = mutableListOf<List<Cell>>()
|
||||
|
||||
// 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<String>()
|
||||
|
||||
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<List<Cell>>, font: Font) {
|
||||
val firstRow = tableData[0]
|
||||
val numOfColumns = firstRow.size
|
||||
for (i in tableData.indices) {
|
||||
val dataRow = tableData[i] as ArrayList<Cell>
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,6 +1,8 @@
|
|||
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
|
||||
|
@ -18,6 +20,7 @@ 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
|
||||
|
@ -25,10 +28,15 @@ 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()
|
||||
|
@ -67,9 +75,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) {
|
||||
|
@ -87,7 +95,7 @@ class EReceiptFragment : Fragment() {
|
|||
df.decimalFormatSymbols = DecimalFormatSymbols(Locale.getDefault())
|
||||
|
||||
val amount = transferDetail.assetAmount.toDouble() /
|
||||
10.toDouble().pow(transferDetail.assetPrecision.toDouble())
|
||||
Math.pow(10.toDouble(), transferDetail.assetPrecision.toDouble())
|
||||
val assetAmount = "${df.format(amount)} ${transferDetail.getUIAssetSymbol()}"
|
||||
binding.tvAmount.text = assetAmount
|
||||
|
||||
|
@ -96,7 +104,7 @@ class EReceiptFragment : Fragment() {
|
|||
val numberFormat = NumberFormat.getNumberInstance()
|
||||
val currency = Currency.getInstance(transferDetail.fiatSymbol)
|
||||
val fiatEquivalent = transferDetail.fiatAmount.toDouble() /
|
||||
10.0.pow(currency.defaultFractionDigits.toDouble())
|
||||
Math.pow(10.0, currency.defaultFractionDigits.toDouble())
|
||||
|
||||
val equivalentValue = "${numberFormat.format(fiatEquivalent)} ${currency.currencyCode}"
|
||||
binding.tvEquivalentValue.text = equivalentValue
|
||||
|
@ -143,13 +151,47 @@ class EReceiptFragment : Fragment() {
|
|||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
if (item.itemId == R.id.menu_share) {
|
||||
shareEReceiptScreenshot()
|
||||
verifyStoragePermission()
|
||||
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<out String>,
|
||||
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() {
|
||||
|
@ -172,8 +214,4 @@ class EReceiptFragment : Fragment() {
|
|||
shareIntent.type = "*/*"
|
||||
startActivity(Intent.createChooser(shareIntent, getString(R.string.text__share_with)))
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val TAG = "EReceiptFragment"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
package cy.agorise.bitsybitshareswallet.ui.transactions
|
||||
package cy.agorise.bitsybitshareswallet.fragments
|
||||
|
||||
|
||||
import android.content.res.Resources
|
||||
|
@ -18,6 +18,7 @@ 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
|
||||
|
@ -109,7 +110,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) }
|
||||
|
@ -127,7 +128,7 @@ class FilterOptionsDialog : DialogFragment() {
|
|||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// Initialize Equivalent Value
|
||||
binding.cbEquivalentValue.setOnCheckedChangeListener { _, isChecked ->
|
|
@ -342,7 +342,7 @@ class MerchantsFragment : Fragment(), OnMapReadyCallback, SearchView.OnSuggestio
|
|||
try {
|
||||
mMap?.animateCamera(CameraUpdateFactory.newLatLngZoom(LatLng(lat, lon), 15f))
|
||||
} catch (e: Exception) {
|
||||
Log.d(TAG, e.message ?: "onSuggestionClick unknown error")
|
||||
Log.d(TAG, e.message)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
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
|
||||
|
@ -21,6 +22,7 @@ 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
|
||||
|
@ -32,10 +34,23 @@ 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
|
||||
|
@ -105,11 +120,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)
|
||||
|
||||
|
@ -140,11 +155,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<*>?) {}
|
||||
|
@ -336,12 +351,47 @@ class ReceiveTransactionFragment : ConnectedFragment() {
|
|||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
if (item.itemId == R.id.menu_share) {
|
||||
shareQRScreenshot()
|
||||
verifyStoragePermission()
|
||||
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<out String>,
|
||||
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.
|
||||
|
@ -371,15 +421,4 @@ 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"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,10 +1,13 @@
|
|||
package cy.agorise.bitsybitshareswallet.ui.transactions
|
||||
package cy.agorise.bitsybitshareswallet.fragments
|
||||
|
||||
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
|
||||
|
@ -15,11 +18,14 @@ 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.utils.BounceTouchListener
|
||||
import cy.agorise.bitsybitshareswallet.utils.Constants
|
||||
import cy.agorise.bitsybitshareswallet.models.FilterOptions
|
||||
import cy.agorise.bitsybitshareswallet.utils.*
|
||||
import cy.agorise.bitsybitshareswallet.viewmodels.TransactionsViewModel
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.disposables.CompositeDisposable
|
||||
import java.io.File
|
||||
import java.util.*
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
|
@ -29,14 +35,17 @@ 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(
|
||||
|
@ -69,7 +78,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
|
||||
|
@ -85,7 +94,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)
|
||||
|
@ -127,7 +136,7 @@ class TransactionsFragment : Fragment(), FilterOptionsDialog.OnFilterOptionsSele
|
|||
true
|
||||
}
|
||||
R.id.menu_export -> {
|
||||
showExportOptionsDialog()
|
||||
verifyStoragePermission()
|
||||
true
|
||||
}
|
||||
else -> super.onOptionsItemSelected(item)
|
||||
|
@ -153,28 +162,75 @@ class TransactionsFragment : Fragment(), FilterOptionsDialog.OnFilterOptionsSele
|
|||
viewModel.applyFilterOptions(filterOptions)
|
||||
}
|
||||
|
||||
private fun showExportOptionsDialog() {
|
||||
// Make export options selected by default
|
||||
val indices = intArrayOf(0, 1)
|
||||
/** 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<out String>,
|
||||
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() {
|
||||
MaterialDialog(requireContext()).show {
|
||||
title(R.string.title_export_transactions)
|
||||
listItemsMultiChoice(
|
||||
R.array.export_options,
|
||||
initialSelection = indices,
|
||||
waitForPositiveButton = true
|
||||
initialSelection = intArrayOf(0, 1)
|
||||
) { _, indices, _ ->
|
||||
// Update export options selected
|
||||
isPdfRequested = indices.contains(0)
|
||||
isCsvRequested = indices.contains(1)
|
||||
val exportPDF = indices.contains(0)
|
||||
val exportCSV = indices.contains(1)
|
||||
exportFilteredTransactions(exportPDF, exportCSV)
|
||||
}
|
||||
negativeButton(android.R.string.cancel) { dismiss() }
|
||||
positiveButton(R.string.title_export) { getFolderForExport.launch(null) }
|
||||
positiveButton(R.string.title_export)
|
||||
}
|
||||
}
|
||||
|
||||
private val getFolderForExport = registerForActivityResult(OpenDocumentTree()) { folderUri ->
|
||||
viewModel.exportFilteredTransactions(folderUri, isPdfRequested, isCsvRequested)
|
||||
/** 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) }
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
|
@ -182,8 +238,4 @@ class TransactionsFragment : Fragment(), FilterOptionsDialog.OnFilterOptionsSele
|
|||
|
||||
if (!mDisposables.isDisposed) mDisposables.dispose()
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val TAG = "TransactionsFragment"
|
||||
}
|
||||
}
|
|
@ -1,6 +1,7 @@
|
|||
package cy.agorise.bitsybitshareswallet.ui.transactions
|
||||
package cy.agorise.bitsybitshareswallet.models
|
||||
|
||||
import android.os.Parcelable
|
||||
import cy.agorise.bitsybitshareswallet.fragments.TransactionsFragment
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
/**
|
|
@ -3,6 +3,7 @@ package cy.agorise.bitsybitshareswallet.models.coingecko
|
|||
import com.google.gson.JsonDeserializationContext
|
||||
import com.google.gson.JsonDeserializer
|
||||
import com.google.gson.JsonElement
|
||||
import junit.framework.Assert
|
||||
import java.lang.reflect.Type
|
||||
|
||||
class MarketDataDeserializer : JsonDeserializer<MarketData> {
|
||||
|
|
|
@ -14,14 +14,14 @@ import cy.agorise.graphenej.stats.ExponentialMovingAverage
|
|||
* The basic idea here is to keep track of the sequence of activity life cycle callbacks so that we
|
||||
* can infer when the user has left the app and the node connection can be salfely shut down.
|
||||
*/
|
||||
class NetworkServiceManager(nodes: List<String>) : ActivityLifecycleCallbacks {
|
||||
class NetworkServiceManager(nodes: List<String>) :
|
||||
ActivityLifecycleCallbacks {
|
||||
/**
|
||||
* Handler instance used to schedule tasks back to the main thread
|
||||
*/
|
||||
private val mHandler = Handler()
|
||||
private var mNetworkService: NetworkService? = null
|
||||
private val mNodeUrls: Array<String> = nodes.toTypedArray()
|
||||
|
||||
/**
|
||||
* Runnable used to schedule a service disconnection once the app is not visible to the user for
|
||||
* more than DISCONNECT_DELAY milliseconds.
|
||||
|
@ -33,25 +33,9 @@ class NetworkServiceManager(nodes: List<String>) : ActivityLifecycleCallbacks {
|
|||
}
|
||||
}
|
||||
|
||||
fun onApplicationCreated() {
|
||||
startService()
|
||||
}
|
||||
|
||||
override fun onActivityCreated(activity: Activity, bundle: Bundle?) {}
|
||||
override fun onActivityStarted(activity: Activity) {}
|
||||
override fun onActivityResumed(activity: Activity) {
|
||||
startService()
|
||||
}
|
||||
|
||||
override fun onActivityPaused(activity: Activity) {
|
||||
mHandler.postDelayed(mDisconnectRunnable, DISCONNECT_DELAY)
|
||||
}
|
||||
|
||||
override fun onActivityStopped(activity: Activity) {}
|
||||
override fun onActivitySaveInstanceState(activity: Activity, bundle: Bundle) {}
|
||||
override fun onActivityDestroyed(activity: Activity) {}
|
||||
|
||||
private fun startService() {
|
||||
override fun onActivityResumed(activity: Activity?) {
|
||||
mHandler.removeCallbacks(mDisconnectRunnable)
|
||||
if (mNetworkService == null) {
|
||||
mNetworkService = NetworkService.getInstance()
|
||||
|
@ -59,6 +43,17 @@ class NetworkServiceManager(nodes: List<String>) : ActivityLifecycleCallbacks {
|
|||
}
|
||||
}
|
||||
|
||||
override fun onActivityPaused(activity: Activity) {
|
||||
mHandler.postDelayed(
|
||||
mDisconnectRunnable,
|
||||
DISCONNECT_DELAY.toLong()
|
||||
)
|
||||
}
|
||||
|
||||
override fun onActivityStopped(activity: Activity) {}
|
||||
override fun onActivitySaveInstanceState(activity: Activity, bundle: Bundle) {}
|
||||
override fun onActivityDestroyed(activity: Activity) {}
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* Constant used to specify how long will the app wait for another activity to go through its starting life
|
||||
|
@ -66,7 +61,7 @@ class NetworkServiceManager(nodes: List<String>) : ActivityLifecycleCallbacks {
|
|||
*
|
||||
* This is used as a means to detect whether or not the user has left the app.
|
||||
*/
|
||||
private const val DISCONNECT_DELAY = 1500L
|
||||
private const val DISCONNECT_DELAY = 1500
|
||||
}
|
||||
|
||||
}
|
|
@ -1,173 +0,0 @@
|
|||
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<List<TransferDetail>>
|
||||
|
||||
/**
|
||||
* 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<List<TransferDetail>>()
|
||||
|
||||
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<List<TransferDetail>> {
|
||||
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<TransferDetail>,
|
||||
filterOptions: FilterOptions
|
||||
): List<TransferDetail> = withContext(Dispatchers.Default) {
|
||||
|
||||
// Create a list to store the filtered transactions
|
||||
val filteredTransactions = ArrayList<TransferDetail>()
|
||||
|
||||
// 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"
|
||||
}
|
||||
}
|
|
@ -33,7 +33,7 @@ class BitsyApplication : Application() {
|
|||
// Add RxJava error handler to avoid crashes when an error occurs on a RxJava operation, but still log the
|
||||
// exception to Crashlytics so that we can fix the issues
|
||||
RxJavaPlugins.setErrorHandler { throwable ->
|
||||
Log.e("RxJava Error", throwable.message ?: "Unknown RxJava error")
|
||||
Log.e("RxJava Error", throwable.message)
|
||||
FirebaseCrashlytics.getInstance().recordException(throwable)
|
||||
}
|
||||
|
||||
|
@ -61,8 +61,7 @@ class BitsyApplication : Application() {
|
|||
// Fake call to onActivityResumed, because BiTSy is using a single activity and at the moment
|
||||
// onActivityResumed is called the first time the app starts, the NetworkServiceManager has not
|
||||
// been configured yet, thus causing the NetworkService to never connect.
|
||||
// TODO try using ProcessLifecycleObserver
|
||||
networkManager.onApplicationCreated()
|
||||
networkManager.onActivityResumed(null)
|
||||
}
|
||||
|
||||
override fun onTerminate() {
|
||||
|
|
|
@ -0,0 +1,102 @@
|
|||
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 +
|
||||
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<Int>) {
|
||||
// TODO show progress
|
||||
}
|
||||
|
||||
override fun onPostExecute(message: String) {
|
||||
mContext.get()?.toast(message)
|
||||
}
|
||||
}
|
|
@ -48,6 +48,9 @@ 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
|
||||
|
||||
|
@ -119,6 +122,9 @@ 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
|
||||
|
||||
|
|
|
@ -0,0 +1,215 @@
|
|||
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<List<TransferDetail>, Int, String>() {
|
||||
|
||||
companion object {
|
||||
private const val TAG = "PDFGeneratorTask"
|
||||
}
|
||||
|
||||
private val mContextRef: WeakReference<Context> = WeakReference(context)
|
||||
|
||||
override fun doInBackground(vararg params: List<TransferDetail>): 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<TransferDetail>): 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<List<Cell>>()
|
||||
|
||||
// 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<Cell>()
|
||||
|
||||
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<TransferDetail>, font: Font, locale: Locale): List<List<Cell>> {
|
||||
|
||||
val tableData = ArrayList<List<Cell>>()
|
||||
|
||||
// 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<Cell>()
|
||||
|
||||
val cols = ArrayList<String>()
|
||||
|
||||
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<List<Cell>>, font: Font) {
|
||||
val firstRow = tableData[0]
|
||||
val numOfColumns = firstRow.size
|
||||
for (i in tableData.indices) {
|
||||
val dataRow = tableData[i] as ArrayList<Cell>
|
||||
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<Int>) {
|
||||
// TODO show progress
|
||||
}
|
||||
|
||||
override fun onPostExecute(message: String) {
|
||||
mContextRef.get()?.toast(message)
|
||||
}
|
||||
}
|
|
@ -44,7 +44,7 @@ class ReceiveTransactionViewModel(application: Application) : AndroidViewModel(a
|
|||
try {
|
||||
_qrCodeBitmap.value = encodeAsBitmap(Invoice.toQrCode(invoice), "#139657", size) // PalmPay green
|
||||
} catch (e: Exception) {
|
||||
Log.d("ReceiveTransactionVM", e.message ?: "Unknown error in updateInvoice")
|
||||
Log.d("ReceiveTransactionVM", e.message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,136 @@
|
|||
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<List<TransferDetail>>
|
||||
|
||||
/**
|
||||
* 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<List<TransferDetail>>()
|
||||
|
||||
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<List<TransferDetail>> {
|
||||
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<TransferDetail>, filterOptions: FilterOptions) : List<TransferDetail> {
|
||||
return withContext(Dispatchers.Default) {
|
||||
|
||||
// Create a list to store the filtered transactions
|
||||
val filteredTransactions = ArrayList<TransferDetail>()
|
||||
|
||||
// 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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -50,7 +50,7 @@
|
|||
|
||||
<fragment
|
||||
android:id="@+id/transactions_dest"
|
||||
android:name="cy.agorise.bitsybitshareswallet.ui.transactions.TransactionsFragment"
|
||||
android:name="cy.agorise.bitsybitshareswallet.fragments.TransactionsFragment"
|
||||
android:label="@string/title_transactions"
|
||||
tools:layout="@layout/fragment_transactions">
|
||||
|
||||
|
|
|
@ -65,6 +65,7 @@
|
|||
<string name="title_export_transactions">Exportieren Sie gefilterte Transaktionen</string>
|
||||
<string name="text__pdf">PDF</string>
|
||||
<string name="text__csv">CSV</string>
|
||||
<string name="msg__storage_permission_necessary_export">Zum Exportieren von PDF / CSV-Dateien ist eine Speichererlaubnis erforderlich.</string>
|
||||
<string name="title_from">Von</string>
|
||||
<string name="title_to">Zu</string>
|
||||
<string name="title_memo">Memo</string>
|
||||
|
@ -125,6 +126,7 @@
|
|||
<string name="msg__invoice_subject">BiTSy Rechnung von %1$s</string>
|
||||
<string name="title_share">Aktie</string>
|
||||
<string name="text__share_with">Teilen mit</string>
|
||||
<string name="msg__storage_permission_necessary_share">Für die Freigabe von Bildern ist eine Speichererlaubnis erforderlich.</string>
|
||||
|
||||
<!-- Settings -->
|
||||
<string name="title_settings">Einstellungen</string>
|
||||
|
|
|
@ -65,6 +65,7 @@
|
|||
<string name="title_export_transactions">Exportar transacciones filtradas</string>
|
||||
<string name="text__pdf">PDF</string>
|
||||
<string name="text__csv">CSV</string>
|
||||
<string name="msg__storage_permission_necessary_export">El permiso de almacenamiento es necesario para exportar los archivos PDF/CSV.</string>
|
||||
<string name="title_from">De</string>
|
||||
<string name="title_to">Para</string>
|
||||
<string name="title_memo">Memo</string>
|
||||
|
@ -124,6 +125,7 @@
|
|||
<string name="msg__invoice_subject">Invoice BiTSy de %1$s</string>
|
||||
<string name="title_share">Compartir</string>
|
||||
<string name="text__share_with">Compartir con</string>
|
||||
<string name="msg__storage_permission_necessary_share">El permiso de almacenamiento es necesario para compartir imágenes.</string>
|
||||
|
||||
<!-- Settings -->
|
||||
<string name="title_settings">Ajustes</string>
|
||||
|
|
|
@ -65,6 +65,7 @@
|
|||
<string name="title_export_transactions">Exporter les transactions filtrées</string>
|
||||
<string name="text__pdf">PDF</string>
|
||||
<string name="text__csv">CSV</string>
|
||||
<string name="msg__storage_permission_necessary_export">Une autorisation de stockage est nécessaire pour exporter des fichiers PDF / CSV.</string>
|
||||
<string name="title_from">De</string>
|
||||
<string name="title_to">À</string>
|
||||
<string name="title_memo">Note</string>
|
||||
|
@ -125,6 +126,7 @@
|
|||
<string name="msg__invoice_subject">Facture biTSy de %1$s</string>
|
||||
<string name="title_share">Partager</string>
|
||||
<string name="text__share_with">Partager avec</string>
|
||||
<string name="msg__storage_permission_necessary_share">Une autorisation de stockage est nécessaire pour partager des images.</string>
|
||||
|
||||
<!-- Settings -->
|
||||
<string name="title_settings">Réglages</string>
|
||||
|
|
|
@ -65,6 +65,7 @@
|
|||
<string name="title_export_transactions">फ़िल्टर्ड लेनदेन को निर्यात करें</string>
|
||||
<string name="text__pdf">पीडीएफ</string>
|
||||
<string name="text__csv">सीएसवी</string>
|
||||
<string name="msg__storage_permission_necessary_export">PDF / CSV फ़ाइलों को निर्यात करने के लिए भंडारण अनुमति आवश्यक है।</string>
|
||||
<string name="title_from">से</string>
|
||||
<string name="title_to">सेवा मेरे</string>
|
||||
<string name="title_memo">मेमो</string>
|
||||
|
@ -125,6 +126,7 @@
|
|||
<string name="msg__invoice_subject">%1$s से बीटीएसआई चालान</string>
|
||||
<string name="title_share">शेयर</string>
|
||||
<string name="text__share_with">के साथ शेयर करें</string>
|
||||
<string name="msg__storage_permission_necessary_share">छवियों को साझा करने के लिए भंडारण की अनुमति आवश्यक है।</string>
|
||||
|
||||
<!-- Settings -->
|
||||
<string name="title_settings">सेटिंग्स</string>
|
||||
|
|
|
@ -65,6 +65,7 @@
|
|||
<string name="title_export_transactions">フィルタリングされたトランザクションをエクスポートする</string>
|
||||
<string name="text__pdf">PDF</string>
|
||||
<string name="text__csv">CSV</string>
|
||||
<string name="msg__storage_permission_necessary_export">PDF / CSVファイルをエクスポートするには、ストレージ許可が必要です。</string>
|
||||
<string name="title_from">から</string>
|
||||
<string name="title_to">に</string>
|
||||
<string name="title_memo">メモ</string>
|
||||
|
@ -125,6 +126,7 @@
|
|||
<string name="msg__invoice_subject">%1$sからのBiTSy請求書</string>
|
||||
<string name="title_share">シェア</string>
|
||||
<string name="text__share_with">と共有する</string>
|
||||
<string name="msg__storage_permission_necessary_share">画像を共有するには保存許可が必要です。</string>
|
||||
|
||||
<!-- Settings -->
|
||||
<string name="title_settings">設定</string>
|
||||
|
|
|
@ -65,6 +65,7 @@
|
|||
<string name="title_export_transactions">ਫਿਲਟਰ ਟ੍ਰਾਂਜੈਕਸ਼ਨਾਂ ਨੂੰ ਐਕਸਪੋਰਟ ਕਰੋ</string>
|
||||
<string name="text__pdf">ਪੀਡੀਐਫ</string>
|
||||
<string name="text__csv">CSV</string>
|
||||
<string name="msg__storage_permission_necessary_export">ਸਟੋਰੇਜ਼ ਦੀ ਇਜ਼ਾਜ਼ਤ PDF / CSV ਫਾਈਲਾਂ ਨੂੰ ਨਿਰਯਾਤ ਕਰਨ ਲਈ ਜ਼ਰੂਰੀ ਹੈ.</string>
|
||||
<string name="title_from">ਤੋਂ</string>
|
||||
<string name="title_to">ਨੂੰ</string>
|
||||
<string name="title_memo">ਮੀਮੋ</string>
|
||||
|
@ -125,6 +126,7 @@
|
|||
<string name="msg__invoice_subject">%1$s ਦਾ ਬੀ.ਟੀ.ਸੀ ਚਲਾਨ</string>
|
||||
<string name="title_share">ਸਾਂਝਾ ਕਰੋ</string>
|
||||
<string name="text__share_with">ਨਾਲ ਸਾਂਝਾ ਕਰੋ</string>
|
||||
<string name="msg__storage_permission_necessary_share">ਤਸਵੀਰਾਂ ਨੂੰ ਸਾਂਝਾ ਕਰਨ ਲਈ ਸਟੋਰੇਜ਼ ਦੀ ਆਗਿਆ ਜ਼ਰੂਰੀ ਹੈ.</string>
|
||||
|
||||
<!-- Settings -->
|
||||
<string name="title_settings">ਸੈਟਿੰਗਜ਼</string>
|
||||
|
|
|
@ -65,6 +65,7 @@
|
|||
<string name="title_export_transactions">Exportar transações filtradas</string>
|
||||
<string name="text__pdf">PDF</string>
|
||||
<string name="text__csv">CSV</string>
|
||||
<string name="msg__storage_permission_necessary_export">É necessária permissão de armazenamento para exportar arquivos PDF / CSV.</string>
|
||||
<string name="title_from">De</string>
|
||||
<string name="title_to">Para</string>
|
||||
<string name="title_memo">Memorando</string>
|
||||
|
@ -125,6 +126,7 @@
|
|||
<string name="msg__invoice_subject">Fatura BiTSy de %1$s</string>
|
||||
<string name="title_share">Compartilhar</string>
|
||||
<string name="text__share_with">Compartilhar com</string>
|
||||
<string name="msg__storage_permission_necessary_share">É necessária permissão de armazenamento para compartilhar imagens.</string>
|
||||
|
||||
<!-- Settings -->
|
||||
<string name="title_settings">Definições</string>
|
||||
|
|
|
@ -65,6 +65,7 @@
|
|||
<string name="title_export_transactions">Экспорт отфильтрованных транзакций</string>
|
||||
<string name="text__pdf">PDF</string>
|
||||
<string name="text__csv">CSV</string>
|
||||
<string name="msg__storage_permission_necessary_export">Разрешение на хранение необходимо для экспорта файлов PDF / CSV.</string>
|
||||
<string name="title_from">От</string>
|
||||
<string name="title_to">к</string>
|
||||
<string name="title_memo">напоминание</string>
|
||||
|
@ -125,6 +126,7 @@
|
|||
<string name="msg__invoice_subject">Счет BiTSy от %1$s</string>
|
||||
<string name="title_share">Поделиться</string>
|
||||
<string name="text__share_with">Поделиться с</string>
|
||||
<string name="msg__storage_permission_necessary_share">Разрешение на хранение необходимо для обмена изображениями.</string>
|
||||
|
||||
<!-- Settings -->
|
||||
<string name="title_settings">настройки</string>
|
||||
|
|
|
@ -65,6 +65,7 @@
|
|||
<string name="title_export_transactions">导出已过滤的交易</string>
|
||||
<string name="text__pdf">PDF</string>
|
||||
<string name="text__csv">CSV</string>
|
||||
<string name="msg__storage_permission_necessary_export">导出PDF / CSV文件需要存储权限。</string>
|
||||
<string name="title_from">从</string>
|
||||
<string name="title_to">至</string>
|
||||
<string name="title_memo">备忘录</string>
|
||||
|
@ -125,6 +126,7 @@
|
|||
<string name="msg__invoice_subject">来自%1$s的BiTSy发票</string>
|
||||
<string name="title_share">分享</string>
|
||||
<string name="text__share_with">与某人分享</string>
|
||||
<string name="msg__storage_permission_necessary_share">共享图像需要存储权限。</string>
|
||||
|
||||
<!-- Settings -->
|
||||
<string name="title_settings">设置</string>
|
||||
|
|
|
@ -67,6 +67,7 @@
|
|||
<string name="title_export_transactions">Export filtered transactions</string>
|
||||
<string name="text__pdf">PDF</string>
|
||||
<string name="text__csv">CSV</string>
|
||||
<string name="msg__storage_permission_necessary_export">Storage permission is necessary to export PDF/CSV files.</string>
|
||||
<string name="title_from">From</string>
|
||||
<string name="title_to">To</string>
|
||||
<string name="title_memo">Memo</string>
|
||||
|
@ -127,6 +128,7 @@
|
|||
<string name="msg__invoice_subject">BiTSy invoice from %1$s</string>
|
||||
<string name="title_share">Share</string>
|
||||
<string name="text__share_with">Share with</string>
|
||||
<string name="msg__storage_permission_necessary_share">Storage permission is necessary to share images.</string>
|
||||
|
||||
<!-- Settings -->
|
||||
<string name="title_settings">Settings</string>
|
||||
|
|
|
@ -12,7 +12,7 @@ class MarketDataDeserializerTest {
|
|||
val str = "{\"current_price\": {\"aed\": 0.14139359620401012,\"ars\": 1.476552955052185,\"aud\": 0.05410080634896981,\"bch\": 0.0003021370317928406,\"bdt\": 3.2298217535732276,\"bhd\": 0.01451147244444769,\"bmd\": 0.03849350092032233,\"bnb\": 0.007113127493734956,\"brl\": 0.15000509277539803,\"btc\": 0.00001043269732289735,\"cad\": 0.051866143140042266,\"chf\": 0.03825734329217614,\"clp\": 26.587581916766037,\"cny\": 0.2652895096426772,\"czk\": 0.8706365729081245,\"dkk\": 0.25236393094264586,\"eos\": 0.01566778197589746,\"eth\": 0.0003870069548974383,\"eur\": 0.033804376612212375,\"gbp\": 0.030484350651335475,\"hkd\": 0.3012660745118239,\"huf\": 10.909058160819312,\"idr\": 558.1942568455942,\"ils\": 0.14452962323048843,\"inr\": 2.721290348862006,\"jpy\": 4.327150672205728,\"krw\": 43.47379006939362,\"kwd\": 0.011703102097803801,\"lkr\": 6.939897047172613,\"ltc\": 0.0013225337650442446,\"mmk\": 60.56217136246436,\"mxn\": 0.7738105980956592,\"myr\": 0.1608450935955668,\"nok\": 0.335428517669597,\"nzd\": 0.056803550529088344,\"php\": 2.046274976098886,\"pkr\": 5.3730315641051885,\"pln\": 0.1449376543402434,\"rub\": 2.596498268228413,\"sar\": 0.1444545609036934,\"sek\": 0.3498212376637053,\"sgd\": 0.05281188996415366,\"thb\": 1.2598922851221481,\"try\": 0.20393883733037357,\"twd\": 1.1869880579631216,\"usd\": 0.03849350092032233,\"vef\": 9565.159285292651,\"xag\": 0.002632124388265174,\"xau\": 0.00003094261577979185,\"xdr\": 0.02769368731511483,\"xlm\": 0.3411570542267162,\"xrp\": 0.11074614753363282,\"zar\": 0.5534635980499906}}"
|
||||
val gson = GsonBuilder().registerTypeAdapter(MarketData::class.java, MarketDataDeserializer())
|
||||
.create()
|
||||
val marketData = gson.fromJson(str, MarketData::class.java)
|
||||
val marketData = gson.fromJson<MarketData>(str, MarketData::class.java)
|
||||
Assert.assertEquals(0.03849350092032233, marketData.current_price["usd"])
|
||||
Assert.assertEquals(0.033804376612212375, marketData.current_price["eur"])
|
||||
}
|
||||
|
|
|
@ -13,11 +13,11 @@ buildscript {
|
|||
maven { url 'https://plugins.gradle.org/m2/' }
|
||||
}
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:7.2.1'
|
||||
classpath 'com.android.tools.build:gradle:4.1.3'
|
||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||
classpath "androidx.navigation:navigation-safe-args-gradle-plugin:$nav_version"
|
||||
classpath 'com.google.gms:google-services:4.3.10'
|
||||
classpath 'com.google.firebase:firebase-crashlytics-gradle:2.5.2'
|
||||
classpath 'com.google.gms:google-services:4.3.5'
|
||||
classpath 'com.google.firebase:firebase-crashlytics-gradle:2.5.1'
|
||||
|
||||
// NOTE: Do not place your application dependencies here; they belong
|
||||
// in the individual module build.gradle files
|
||||
|
|
2
gradle/wrapper/gradle-wrapper.properties
vendored
2
gradle/wrapper/gradle-wrapper.properties
vendored
|
@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME
|
|||
distributionPath=wrapper/dists
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.4.2-bin.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-6.5-bin.zip
|
||||
|
|
|
@ -1 +1 @@
|
|||
Subproject commit 848a95e766255db6d0a659eb12d65438a54e27b0
|
||||
Subproject commit e5d68e899274c2b9abcae2735e9186f574626e2b
|
Loading…
Reference in a new issue