Compare commits
No commits in common. "ec54ed0620b02e0db25bbe0e6d3f1b12e1c5faa0" and "96faf9bffd347461a036c7c32ecc70a7224d6188" have entirely different histories.
ec54ed0620
...
96faf9bffd
35 changed files with 722 additions and 544 deletions
app
build.gradle
build.gradlesrc
main
AndroidManifest.xml
java/cy/agorise/bitsybitshareswallet
adapters
domain/usecase
fragments
EReceiptFragment.ktFilterOptionsDialog.ktMerchantsFragment.ktReceiveTransactionFragment.ktTransactionsFragment.kt
models
network
ui/transactions
utils
viewmodels
res
navigation
values-de
values-es
values-fr
values-hi
values-ja
values-pa
values-pt
values-ru
values-zh
values
test/java/cy/agorise/bitsybitshareswallet
gradle/wrapper
graphenejlib
|
@ -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…
Add table
Add a link
Reference in a new issue