/*
- * Copyright © 2019 Damyan Ivanov.
+ * Copyright © 2021 Damyan Ivanov.
* This file is part of MoLe.
* MoLe is free software: you can distribute it and/or modify it
* under the term of the GNU General Public License as published by
package net.ktnx.mobileledger.ui.activity;
import android.annotation.SuppressLint;
+import android.app.Activity;
import android.database.Cursor;
+import android.text.TextUtils;
import android.view.LayoutInflater;
import android.view.ViewGroup;
-import android.widget.LinearLayout;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.recyclerview.widget.ItemTouchHelper;
import androidx.recyclerview.widget.RecyclerView;
-import net.ktnx.mobileledger.App;
+import com.google.android.material.snackbar.Snackbar;
+
import net.ktnx.mobileledger.BuildConfig;
import net.ktnx.mobileledger.R;
import net.ktnx.mobileledger.async.DescriptionSelectedCallback;
+import net.ktnx.mobileledger.databinding.NewTransactionRowBinding;
import net.ktnx.mobileledger.model.Currency;
import net.ktnx.mobileledger.model.Data;
import net.ktnx.mobileledger.model.LedgerTransaction;
import net.ktnx.mobileledger.model.LedgerTransactionAccount;
import net.ktnx.mobileledger.model.MobileLedgerProfile;
import net.ktnx.mobileledger.utils.Logger;
+import net.ktnx.mobileledger.utils.MLDB;
import net.ktnx.mobileledger.utils.Misc;
import java.util.ArrayList;
class NewTransactionItemsAdapter extends RecyclerView.Adapter<NewTransactionItemHolder>
implements DescriptionSelectedCallback {
- NewTransactionModel model;
+ private final NewTransactionModel model;
+ private final ItemTouchHelper touchHelper;
private MobileLedgerProfile mProfile;
- private ItemTouchHelper touchHelper;
private RecyclerView recyclerView;
private int checkHoldCounter = 0;
NewTransactionItemsAdapter(NewTransactionModel viewModel, MobileLedgerProfile profile) {
public void setProfile(MobileLedgerProfile profile) {
mProfile = profile;
}
- int addRow() {
+ private int addRow() {
return addRow(null);
}
- int addRow(String commodity) {
+ private int addRow(String commodity) {
final int newAccountCount = model.addAccount(new LedgerTransactionAccount("", commodity));
Logger.debug("new-transaction",
String.format(Locale.US, "invoking notifyItemInserted(%d)", newAccountCount));
@NonNull
@Override
public NewTransactionItemHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
- LinearLayout row = (LinearLayout) LayoutInflater.from(parent.getContext())
- .inflate(R.layout.new_transaction_row,
- parent, false);
+ NewTransactionRowBinding b =
+ NewTransactionRowBinding.inflate(LayoutInflater.from(parent.getContext()), parent,
+ false);
- return new NewTransactionItemHolder(row, this);
+ return new NewTransactionItemHolder(b, this);
}
@Override
public void onBindViewHolder(@NonNull NewTransactionItemHolder holder, int position) {
public int getItemCount() {
return model.getAccountCount() + 2;
}
- boolean accountListIsEmpty() {
+ private boolean accountListIsEmpty() {
for (int i = 0; i < model.getAccountCount(); i++) {
LedgerTransactionAccount acc = model.getAccount(i);
if (!acc.getAccountName()
this.recyclerView = null;
}
public void descriptionSelected(String description) {
- debug("descr selected", description);
+ debug("description selected", description);
if (!accountListIsEmpty())
return;
String accFilter = mProfile.getPreferredAccountsFilter();
ArrayList<String> params = new ArrayList<>();
- StringBuilder sb = new StringBuilder(
- "select t.profile, t.id from transactions t where t.description=?");
+ StringBuilder sb = new StringBuilder("select t.profile, t.id from transactions t");
+
+ if (!TextUtils.isEmpty(accFilter)) {
+ sb.append(" JOIN transaction_accounts ta")
+ .append(" ON ta.profile = t.profile")
+ .append(" AND ta.transaction_id = t.id");
+ }
+
+ sb.append(" WHERE t.description=?");
params.add(description);
- if (accFilter != null) {
- sb.append(" AND EXISTS (")
- .append("SELECT 1 FROM transaction_accounts ta ")
- .append("WHERE ta.profile = t.profile")
- .append(" AND ta.transaction_id = t.id")
- .append(" AND UPPER(ta.account_name) LIKE '%'||?||'%')");
- params.add(accFilter.toUpperCase());
+ if (!TextUtils.isEmpty(accFilter)) {
+ sb.append(" AND ta.account_name LIKE '%'||?||'%'");
+ params.add(accFilter);
}
- sb.append(" ORDER BY date desc limit 1");
+ sb.append(" ORDER BY t.year desc, t.month desc, t.day desc LIMIT 1");
final String sql = sb.toString();
- debug("descr", sql);
- debug("descr", params.toString());
-
- try (Cursor c = App.getDatabase()
- .rawQuery(sql, params.toArray(new String[]{})))
- {
- if (!c.moveToNext())
- return;
-
- String profileUUID = c.getString(0);
- int transactionId = c.getInt(1);
- LedgerTransaction tr;
- MobileLedgerProfile profile = Data.getProfile(profileUUID);
- if (profile == null)
- throw new RuntimeException(String.format(
- "Unable to find profile %s, which is supposed to contain " +
- "transaction %d with description %s", profileUUID, transactionId,
- description));
-
- tr = profile.loadTransaction(transactionId);
- ArrayList<LedgerTransactionAccount> accounts = tr.getAccounts();
- NewTransactionModel.Item firstNegative = null;
- NewTransactionModel.Item firstPositive = null;
- int singleNegativeIndex = -1;
- int singlePositiveIndex = -1;
- int negativeCount = 0;
- for (int i = 0; i < accounts.size(); i++) {
- LedgerTransactionAccount acc = accounts.get(i);
- NewTransactionModel.Item item;
- if (model.getAccountCount() < i + 1) {
- model.addAccount(acc);
- notifyItemInserted(i + 1);
- }
- item = model.getItem(i + 1);
+ debug("description", sql);
+ debug("description", params.toString());
+
+ Activity activity = (Activity) recyclerView.getContext();
+ // FIXME: handle exceptions?
+ MLDB.queryInBackground(sql, params.toArray(new String[]{}), new MLDB.CallbackHelper() {
+ @Override
+ public void onStart() {
+ model.incrementBusyCounter();
+ }
+ @Override
+ public void onDone() {
+ model.decrementBusyCounter();
+ }
+ @Override
+ public boolean onRow(@NonNull Cursor cursor) {
+ final String profileUUID = cursor.getString(0);
+ final int transactionId = cursor.getInt(1);
+ activity.runOnUiThread(() -> loadTransactionIntoModel(profileUUID, transactionId));
+ return false; // limit 1, by the way
+ }
+ @Override
+ public void onNoRows() {
+ if (TextUtils.isEmpty(accFilter))
+ return;
+
+ debug("description", "Trying transaction search without preferred account filter");
+
+ final String broaderSql =
+ "select t.profile, t.id from transactions t where t.description=?" +
+ " ORDER BY year desc, month desc, day desc LIMIT 1";
+ params.remove(1);
+ debug("description", broaderSql);
+ debug("description", description);
+
+ activity.runOnUiThread(
+ () -> Snackbar.make(recyclerView, R.string.ignoring_preferred_account,
+ Snackbar.LENGTH_LONG)
+ .show());
+
+ MLDB.queryInBackground(broaderSql, new String[]{description},
+ new MLDB.CallbackHelper() {
+ @Override
+ public void onStart() {
+ model.incrementBusyCounter();
+ }
+ @Override
+ public boolean onRow(@NonNull Cursor cursor) {
+ final String profileUUID = cursor.getString(0);
+ final int transactionId = cursor.getInt(1);
+ activity.runOnUiThread(
+ () -> loadTransactionIntoModel(profileUUID, transactionId));
+ return false;
+ }
+ @Override
+ public void onDone() {
+ model.decrementBusyCounter();
+ }
+ });
+ }
+ });
+ }
+ private void loadTransactionIntoModel(String profileUUID, int transactionId) {
+ LedgerTransaction tr;
+ MobileLedgerProfile profile = Data.getProfile(profileUUID);
+ if (profile == null)
+ throw new RuntimeException(String.format(
+ "Unable to find profile %s, which is supposed to contain transaction %d",
+ profileUUID, transactionId));
+
+ tr = profile.loadTransaction(transactionId);
+ List<LedgerTransactionAccount> accounts = tr.getAccounts();
+ NewTransactionModel.Item firstNegative = null;
+ NewTransactionModel.Item firstPositive = null;
+ int singleNegativeIndex = -1;
+ int singlePositiveIndex = -1;
+ int negativeCount = 0;
+ for (int i = 0; i < accounts.size(); i++) {
+ LedgerTransactionAccount acc = accounts.get(i);
+ NewTransactionModel.Item item;
+ if (model.getAccountCount() < i + 1) {
+ model.addAccount(acc);
+ notifyItemInserted(i + 1);
+ }
+ item = model.getItem(i + 1);
+ item.getAccount()
+ .setAccountName(acc.getAccountName());
+ item.setComment(acc.getComment());
+ if (acc.isAmountSet()) {
item.getAccount()
- .setAccountName(acc.getAccountName());
- if (acc.isAmountSet()) {
- item.getAccount()
- .setAmount(acc.getAmount());
- if (acc.getAmount() < 0) {
- if (firstNegative == null) {
- firstNegative = item;
- singleNegativeIndex = i;
- }
- else
- singleNegativeIndex = -1;
+ .setAmount(acc.getAmount());
+ if (acc.getAmount() < 0) {
+ if (firstNegative == null) {
+ firstNegative = item;
+ singleNegativeIndex = i;
}
- else {
- if (firstPositive == null) {
- firstPositive = item;
- singlePositiveIndex = i;
- }
- else
- singlePositiveIndex = -1;
+ else
+ singleNegativeIndex = -1;
+ }
+ else {
+ if (firstPositive == null) {
+ firstPositive = item;
+ singlePositiveIndex = i;
}
+ else
+ singlePositiveIndex = -1;
}
- else
- item.getAccount()
- .resetAmount();
- notifyItemChanged(i + 1);
}
+ else
+ item.getAccount()
+ .resetAmount();
+ notifyItemChanged(i + 1);
+ }
- if (singleNegativeIndex != -1) {
- firstNegative.getAccount()
- .resetAmount();
- model.moveItemLast(singleNegativeIndex);
- }
- else if (singlePositiveIndex != -1) {
- firstPositive.getAccount()
- .resetAmount();
- model.moveItemLast(singlePositiveIndex);
- }
+ if (singleNegativeIndex != -1) {
+ firstNegative.getAccount()
+ .resetAmount();
+ model.moveItemLast(singleNegativeIndex);
}
+ else if (singlePositiveIndex != -1) {
+ firstPositive.getAccount()
+ .resetAmount();
+ model.moveItemLast(singlePositiveIndex);
+ }
+
checkTransactionSubmittable();
model.setFocusedItem(1);
}
// TODO perhaps do only one notification about the whole range (notifyDatasetChanged)?
}
}
- public void reset() {
+ void reset() {
int presentItemCount = model.getAccountCount();
model.reset();
notifyItemChanged(0); // header changed
if (presentItemCount > 2)
notifyItemRangeRemoved(3, presentItemCount - 2); // all the rest are gone
}
- public void updateFocusedItem(int position) {
+ void updateFocusedItem(int position) {
model.updateFocusedItem(position);
}
- public void noteFocusIsOnAccount(int position) {
+ void noteFocusIsOnAccount(int position) {
model.noteFocusChanged(position, NewTransactionModel.FocusedElement.Account);
}
- public void noteFocusIsOnAmount(int position) {
+ void noteFocusIsOnAmount(int position) {
model.noteFocusChanged(position, NewTransactionModel.FocusedElement.Amount);
}
- public void noteFocusIsOnComment(int position) {
+ void noteFocusIsOnComment(int position) {
model.noteFocusChanged(position, NewTransactionModel.FocusedElement.Comment);
}
- public void toggleComment(int position) {
- model.toggleComment(position);
+ void noteFocusIsOnTransactionComment(int position) {
+ model.noteFocusChanged(position, NewTransactionModel.FocusedElement.TransactionComment);
+ }
+ public void noteFocusIsOnDescription(int pos) {
+ model.noteFocusChanged(pos, NewTransactionModel.FocusedElement.Description);
}
private void holdSubmittableChecks() {
checkHoldCounter++;
final String descriptionText = model.getDescription();
boolean submittable = true;
final ItemsForCurrency itemsForCurrency = new ItemsForCurrency();
- final ItemsForCurrency itemsWithEmptyAmountForCurrency =
- new ItemsForCurrency();
- final ItemsForCurrency itemsWithAccountAndEmptyAmountForCurrency =
- new ItemsForCurrency();
- final ItemsForCurrency itemsWithEmptyAccountForCurrency =
- new ItemsForCurrency();
- final ItemsForCurrency itemsWithAmountForCurrency =
- new ItemsForCurrency();
- final ItemsForCurrency itemsWithAccountForCurrency =
- new ItemsForCurrency();
- final ItemsForCurrency emptyRowsForCurrency =
- new ItemsForCurrency();
+ final ItemsForCurrency itemsWithEmptyAmountForCurrency = new ItemsForCurrency();
+ final ItemsForCurrency itemsWithAccountAndEmptyAmountForCurrency = new ItemsForCurrency();
+ final ItemsForCurrency itemsWithEmptyAccountForCurrency = new ItemsForCurrency();
+ final ItemsForCurrency itemsWithAmountForCurrency = new ItemsForCurrency();
+ final ItemsForCurrency itemsWithAccountForCurrency = new ItemsForCurrency();
+ final ItemsForCurrency emptyRowsForCurrency = new ItemsForCurrency();
final List<NewTransactionModel.Item> emptyRows = new ArrayList<>();
try {
itemsWithAccountForCurrency.add(currName, item);
}
- if (acc.isAmountSet()) {
+ if (!acc.isAmountValid()) {
+ Logger.debug("submittable",
+ String.format("Not submittable: row %d has an invalid amount", i + 1));
+ submittable = false;
+ }
+ else if (acc.isAmountSet()) {
itemsWithAmountForCurrency.add(currName, item);
balance.add(currName, acc.getAmount());
}
model.isSubmittable.setValue(false);
}
}
- private class BalanceForCurrency {
- private HashMap<String, Float> hashMap = new HashMap<>();
+
+ private static class BalanceForCurrency {
+ private final HashMap<String, Float> hashMap = new HashMap<>();
float get(String currencyName) {
Float f = hashMap.get(currencyName);
if (f == null) {
}
}
- private class ItemsForCurrency {
- private HashMap<String, List<NewTransactionModel.Item>> hashMap = new HashMap<>();
+ private static class ItemsForCurrency {
+ private final HashMap<String, List<NewTransactionModel.Item>> hashMap = new HashMap<>();
@NonNull
List<NewTransactionModel.Item> getList(@Nullable String currencyName) {
List<NewTransactionModel.Item> list = hashMap.get(currencyName);