diff --git a/app/src/main/java/cy/agorise/bitsybitshareswallet/fragments/TransactionsFragment.kt b/app/src/main/java/cy/agorise/bitsybitshareswallet/fragments/TransactionsFragment.kt index 0f20150..41253e5 100644 --- a/app/src/main/java/cy/agorise/bitsybitshareswallet/fragments/TransactionsFragment.kt +++ b/app/src/main/java/cy/agorise/bitsybitshareswallet/fragments/TransactionsFragment.kt @@ -28,9 +28,7 @@ import io.reactivex.android.schedulers.AndroidSchedulers import io.reactivex.disposables.CompositeDisposable import kotlinx.android.synthetic.main.fragment_transactions.* import java.io.File -import java.util.* import java.util.concurrent.TimeUnit -import kotlin.collections.ArrayList /** * Shows the list of transactions as well as options to filter and export those transactions @@ -44,15 +42,7 @@ class TransactionsFragment : Fragment(), FilterOptionsDialog.OnFilterOptionsSele private const val REQUEST_WRITE_EXTERNAL_STORAGE_PERMISSION = 100 } - private lateinit var mTransactionsViewModel: TransactionsViewModel - - private lateinit var transfersDetailsAdapter: TransfersDetailsAdapter - - private val transfersDetails = ArrayList() - private val filteredTransfersDetails = ArrayList() - - /** Variables used to filter the transaction items */ - private var mFilterOptions = FilterOptions() + private lateinit var mViewModel: TransactionsViewModel private var mDisposables = CompositeDisposable() @@ -70,36 +60,29 @@ class TransactionsFragment : Fragment(), FilterOptionsDialog.OnFilterOptionsSele val userId = PreferenceManager.getDefaultSharedPreferences(context) .getString(Constants.KEY_CURRENT_ACCOUNT_ID, "") ?: "" - transfersDetailsAdapter = TransfersDetailsAdapter(context!!) + val transfersDetailsAdapter = TransfersDetailsAdapter(context!!) rvTransactions.adapter = transfersDetailsAdapter rvTransactions.layoutManager = LinearLayoutManager(context) // Configure TransactionsViewModel to fetch the transaction history - mTransactionsViewModel = ViewModelProviders.of(this).get(TransactionsViewModel::class.java) + mViewModel = ViewModelProviders.of(this).get(TransactionsViewModel::class.java) - mTransactionsViewModel.getAll(userId).observe(this, Observer> { transfersDetails -> - this.transfersDetails.clear() - this.transfersDetails.addAll(transfersDetails) - applyFilterOptions(false) + mViewModel.getFilteredTransactions(userId).observe(this, + Observer> { transactions -> + if (transactions.isEmpty()) { + rvTransactions.visibility = View.GONE + tvEmpty.visibility = View.VISIBLE + } else { + rvTransactions.visibility = View.VISIBLE + tvEmpty.visibility = View.GONE - if (transfersDetails.isEmpty()) { - rvTransactions.visibility = View.GONE - tvEmpty.visibility = View.VISIBLE - } else { - rvTransactions.visibility = View.VISIBLE - tvEmpty.visibility = View.GONE - } + transfersDetailsAdapter.replaceAll(transactions) + } }) // Set custom touch listener to handle bounce/stretch effect val bounceTouchListener = BounceTouchListener(rvTransactions) rvTransactions.setOnTouchListener(bounceTouchListener) - - // Initialize filter options - val calendar = Calendar.getInstance() - mFilterOptions.endDate = calendar.timeInMillis - calendar.add(Calendar.MONTH, -2) - mFilterOptions.startDate = calendar.timeInMillis } override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { @@ -115,8 +98,7 @@ class TransactionsFragment : Fragment(), FilterOptionsDialog.OnFilterOptionsSele .map { it.queryText.toString().toLowerCase() } .observeOn(AndroidSchedulers.mainThread()) .subscribe { - mFilterOptions.query = it - applyFilterOptions() + mViewModel.setFilterQuery(it) } ) @@ -129,7 +111,7 @@ class TransactionsFragment : Fragment(), FilterOptionsDialog.OnFilterOptionsSele R.id.menu_filter -> { val filterOptionsDialog = FilterOptionsDialog() val args = Bundle() - args.putParcelable(FilterOptionsDialog.KEY_FILTER_OPTIONS, mFilterOptions) + args.putParcelable(FilterOptionsDialog.KEY_FILTER_OPTIONS, mViewModel.getFilterOptions()) filterOptionsDialog.arguments = args filterOptionsDialog.show(childFragmentManager, "filter-options-tag") true @@ -154,68 +136,11 @@ class TransactionsFragment : Fragment(), FilterOptionsDialog.OnFilterOptionsSele return size.x } - /** - * Filters the TransferDetail list given the user selected filter options. - * TODO move this to a background thread - */ - private fun applyFilterOptions(scrollToTop: Boolean = true) { - // Clean the filtered list - filteredTransfersDetails.clear() - - // Make sure the filter dates use the same format as the transactions' dates - val startDate = mFilterOptions.startDate / 1000 - val endDate = mFilterOptions.endDate / 1000 - - for (transferDetail in transfersDetails) { - // Filter by transfer direction - if (transferDetail.direction) { // Transfer sent - if (mFilterOptions.transactionsDirection == 1) - // Looking for received transfers only - continue - } else { // Transfer received - if (mFilterOptions.transactionsDirection == 2) - // Looking for sent transactions only - continue - } - - // Filter by date range - if (!mFilterOptions.dateRangeAll && (transferDetail.date < startDate || - transferDetail.date > endDate)) - continue - - // Filter by asset - if (!mFilterOptions.assetAll && transferDetail.assetSymbol != mFilterOptions.asset) - continue - - // Filter by equivalent value - if (!mFilterOptions.equivalentValueAll && ((transferDetail.fiatAmount ?: -1 ) < mFilterOptions.fromEquivalentValue - || (transferDetail.fiatAmount ?: -1) > mFilterOptions.toEquivalentValue)) - continue - - // Filter transactions sent to agorise - if (mFilterOptions.agoriseFees && transferDetail.to.equals("agorise")) - continue - - // Filter by search query - val text = (transferDetail.from ?: "").toLowerCase() + (transferDetail.to ?: "").toLowerCase() - if (text.contains(mFilterOptions.query, ignoreCase = true)) { - filteredTransfersDetails.add(transferDetail) - } - } - - // Replaces the list of TransferDetail items with the new filtered list - transfersDetailsAdapter.replaceAll(filteredTransfersDetails) - - if (scrollToTop) - rvTransactions.scrollToPosition(0) - } - /** * Gets called when the user selects some filter options in the [FilterOptionsDialog] and wants to apply them. */ override fun onFilterOptionsSelected(filterOptions: FilterOptions) { - mFilterOptions = filterOptions - applyFilterOptions(true) + mViewModel.applyFilterOptions(filterOptions) } /** Verifies that the storage permission has been granted before attempting to generate the export options */ @@ -267,12 +192,13 @@ class TransactionsFragment : Fragment(), FilterOptionsDialog.OnFilterOptionsSele return } - if (exportPDF) - activity?.let { PDFGeneratorTask(it).execute(filteredTransfersDetails) } - - if (exportCSV) - activity?.let { CSVGenerationTask(it).execute(filteredTransfersDetails) } + mViewModel.getFilteredTransactionsOnce()?.let { filteredTransactions -> + if (exportPDF) + activity?.let { PDFGeneratorTask(it).execute(filteredTransactions) } + if (exportCSV) + activity?.let { CSVGenerationTask(it).execute(filteredTransactions) } + } } override fun onDestroy() { diff --git a/app/src/main/java/cy/agorise/bitsybitshareswallet/viewmodels/TransactionsViewModel.kt b/app/src/main/java/cy/agorise/bitsybitshareswallet/viewmodels/TransactionsViewModel.kt index 68bd05f..64f4c76 100644 --- a/app/src/main/java/cy/agorise/bitsybitshareswallet/viewmodels/TransactionsViewModel.kt +++ b/app/src/main/java/cy/agorise/bitsybitshareswallet/viewmodels/TransactionsViewModel.kt @@ -1,15 +1,122 @@ package cy.agorise.bitsybitshareswallet.viewmodels import android.app.Application -import androidx.lifecycle.AndroidViewModel -import androidx.lifecycle.LiveData +import androidx.lifecycle.* import cy.agorise.bitsybitshareswallet.database.joins.TransferDetail +import cy.agorise.bitsybitshareswallet.models.FilterOptions import cy.agorise.bitsybitshareswallet.repositories.TransferDetailRepository +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import java.util.* class TransactionsViewModel(application: Application) : AndroidViewModel(application) { private var mRepository = TransferDetailRepository(application) - internal fun getAll(userId: String): LiveData> { - return mRepository.getAll(userId) + /** + * [FilterOptions] used to filter the list of [TransferDetail] taken from the database + */ + private var mFilterOptions = FilterOptions() + + private lateinit var transactions : LiveData> + + /** + * This [MediatorLiveData] is used to combine two sources of information into one, keeping the + * client of this [ViewModel] receiving only one stream of data (a list of filtered [TransferDetail]) + */ + private val filteredTransactions = MediatorLiveData>() + + init { + // Initialize the start and end dates for the FilterOptions + val calendar = Calendar.getInstance() + mFilterOptions.endDate = calendar.timeInMillis + calendar.add(Calendar.MONTH, -2) + mFilterOptions.startDate = calendar.timeInMillis + } + + internal fun getFilteredTransactions(userId: String): LiveData> { + transactions = mRepository.getAll(userId) + + filteredTransactions.addSource(transactions) { transactions -> + viewModelScope.launch { + filteredTransactions.value = filter(transactions, mFilterOptions) + } + } + + return filteredTransactions + } + + internal fun getFilterOptions(): FilterOptions { + return mFilterOptions + } + + internal fun applyFilterOptions(filterOptions: FilterOptions) = transactions.value?.let { transactions -> + viewModelScope.launch { + filteredTransactions.value = filter(transactions, filterOptions) + } + }.also { mFilterOptions = filterOptions } + + internal fun setFilterQuery(query: String) = transactions.value?.let { transactions -> + mFilterOptions.query = query + viewModelScope.launch { + filteredTransactions.value = filter(transactions, mFilterOptions) + } + } + + internal fun getFilteredTransactionsOnce() = filteredTransactions.value + + /** + * Filters the given list of [TransferDetail] given the [FilterOptions] and returns a filtered list + * of [TransferDetail], doing all the work in a background thread using kotlin coroutines + */ + private suspend fun filter(transactions: List, filterOptions: FilterOptions) : List { + return withContext(Dispatchers.Default) { + + // Create a list to store the filtered transactions + val filteredTransactions = ArrayList() + + // Make sure the filter dates use the same format as the transactions' dates + val startDate = filterOptions.startDate / 1000 + val endDate = filterOptions.endDate / 1000 + + for (transaction in transactions) { + // Filter by transfer direction + if (transaction.direction) { // Transfer sent + if (filterOptions.transactionsDirection == 1) + // Looking for received transfers only + continue + } else { // Transfer received + if (filterOptions.transactionsDirection == 2) + // Looking for sent transactions only + continue + } + + // Filter by date range + if (!filterOptions.dateRangeAll && (transaction.date < startDate || + transaction.date > endDate)) + continue + + // Filter by asset + if (!filterOptions.assetAll && transaction.assetSymbol != filterOptions.asset) + continue + + // Filter by equivalent value + if (!filterOptions.equivalentValueAll && ((transaction.fiatAmount ?: -1 ) < filterOptions.fromEquivalentValue + || (transaction.fiatAmount ?: -1) > filterOptions.toEquivalentValue)) + continue + + // Filter transactions sent to agorise + if (filterOptions.agoriseFees && transaction.to.equals("agorise")) + continue + + // Filter by search query + val text = (transaction.from ?: "").toLowerCase() + (transaction.to ?: "").toLowerCase() + if (text.contains(filterOptions.query, ignoreCase = true)) { + filteredTransactions.add(transaction) + } + } + + filteredTransactions + } } } \ No newline at end of file