+++ /dev/null
-/*
- * Copyright © 2020 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
- * the Free Software Foundation, either version 3 of the License, or
- * (at your opinion), any later version.
- *
- * MoLe is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License terms for details.
- *
- * You should have received a copy of the GNU General Public License
- * along with MoLe. If not, see <https://www.gnu.org/licenses/>.
- */
-
-package net.ktnx.mobileledger.async;
-
-import android.database.Cursor;
-import android.database.sqlite.SQLiteDatabase;
-import android.os.AsyncTask;
-
-import net.ktnx.mobileledger.App;
-
-import java.util.HashMap;
-import java.util.Map;
-
-import static net.ktnx.mobileledger.utils.Logger.debug;
-
-public class RefreshDescriptionsTask extends AsyncTask<Void, Void, Void> {
- @Override
- protected Void doInBackground(Void... voids) {
- Map<String, Boolean> unique = new HashMap<>();
-
- debug("descriptions", "Starting refresh");
- SQLiteDatabase db = App.getDatabase();
-
-// Data.backgroundTaskStarted();
- try {
- db.beginTransactionNonExclusive();
- try {
- db.execSQL("UPDATE description_history set keep=0");
- try (Cursor c = db
- .rawQuery("SELECT distinct description from transactions", null))
- {
- while (c.moveToNext()) {
- String description = c.getString(0);
- String descriptionUpper = description.toUpperCase();
- if (unique.containsKey(descriptionUpper)) continue;
-
- db.execSQL(
- "replace into description_history(description, description_upper, " +
- "keep) values(?, ?, 1)", new String[]{description, descriptionUpper});
- unique.put(descriptionUpper, true);
- }
- }
- db.execSQL("DELETE from description_history where keep=0");
- db.setTransactionSuccessful();
- debug("descriptions", "Refresh successful");
- }
- finally {
- db.endTransaction();
- }
- }
- finally {
-// Data.backgroundTaskFinished();
- debug("descriptions", "Refresh done");
- }
-
- return null;
- }
-}
import net.ktnx.mobileledger.model.LedgerTransaction;
import net.ktnx.mobileledger.model.LedgerTransactionAccount;
import net.ktnx.mobileledger.model.MobileLedgerProfile;
+import net.ktnx.mobileledger.ui.MainModel;
import net.ktnx.mobileledger.utils.NetworkUtil;
import java.io.BufferedReader;
import java.nio.charset.StandardCharsets;
import java.text.ParseException;
import java.util.ArrayList;
+import java.util.Collections;
import java.util.HashMap;
+import java.util.List;
import java.util.Locale;
import java.util.Objects;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
-public class RetrieveTransactionsTask
- extends AsyncTask<Void, RetrieveTransactionsTask.Progress, String> {
+public class RetrieveTransactionsTask extends
+ AsyncTask<Void, RetrieveTransactionsTask.Progress, RetrieveTransactionsTask.Result> {
private static final int MATCHING_TRANSACTIONS_LIMIT = 150;
private static final Pattern reComment = Pattern.compile("^\\s*;");
private static final Pattern reTransactionStart = Pattern.compile(
private Pattern reAccountName = Pattern.compile("/register\\?q=inacct%3A([a-zA-Z0-9%]+)\"");
private Pattern reAccountValue = Pattern.compile(
"<span class=\"[^\"]*\\bamount\\b[^\"]*\">\\s*([-+]?[\\d.,]+)(?:\\s+(\\S+))?</span>");
+ private MainModel mainModel;
private MobileLedgerProfile profile;
+ private List<LedgerAccount> prevAccounts;
private int expectedPostingsCount = -1;
- public RetrieveTransactionsTask(@NonNull MobileLedgerProfile profile) {
+ public RetrieveTransactionsTask(@NonNull MainModel mainModel,
+ @NonNull MobileLedgerProfile profile,
+ List<LedgerAccount> accounts) {
+ this.mainModel = mainModel;
this.profile = profile;
+ this.prevAccounts = accounts;
}
private static void L(String msg) {
//debug("transaction-parser", msg);
Data.backgroundTaskProgress.postValue(values[0]);
}
@Override
- protected void onPostExecute(String error) {
- super.onPostExecute(error);
+ protected void onPostExecute(Result result) {
+ super.onPostExecute(result);
Progress progress = new Progress();
progress.setState(ProgressState.FINISHED);
- progress.setError(error);
+ progress.setError(result.error);
onProgressUpdate(progress);
}
@Override
progress.setState(ProgressState.FINISHED);
onProgressUpdate(progress);
}
- private String retrieveTransactionListLegacy() throws IOException, HTTPException {
+ private void retrieveTransactionListLegacy(List<LedgerAccount> accounts,
+ List<LedgerTransaction> transactions)
+ throws IOException, HTTPException {
Progress progress = Progress.indeterminate();
progress.setState(ProgressState.RUNNING);
progress.setTotal(expectedPostingsCount);
int maxTransactionId = -1;
- ArrayList<LedgerAccount> list = new ArrayList<>();
HashMap<String, LedgerAccount> map = new HashMap<>();
- ArrayList<LedgerAccount> displayed = new ArrayList<>();
- ArrayList<LedgerTransaction> transactions = new ArrayList<>();
LedgerAccount lastAccount = null;
ArrayList<LedgerAccount> syntheticAccounts = new ArrayList<>();
}
lastAccount = new LedgerAccount(profile, accName, parentAccount);
- list.add(lastAccount);
+ accounts.add(lastAccount);
map.put(accName, lastAccount);
state = ParserState.EXPECTING_ACCOUNT_AMOUNT;
if (m.find()) {
if (transactionId == 0)
throw new TransactionParserException(
- "Transaction Id is 0 while expecting " + "description");
+ "Transaction Id is 0 while expecting description");
String date = Objects.requireNonNull(m.group(1));
try {
new LedgerTransaction(transactionId, date, m.group(2));
}
catch (ParseException e) {
- e.printStackTrace();
- return String.format("Error parsing date '%s'", date);
+ throw new TransactionParserException(
+ String.format("Error parsing date '%s'", date));
}
state = ParserState.EXPECTING_TRANSACTION_DETAILS;
L(String.format(Locale.ENGLISH,
}
throwIfCancelled();
-
- profile.setAndStoreAccountAndTransactionListFromWeb(list, transactions);
-
- return null;
}
}
private @NonNull
createdAccounts.add(acc);
return acc;
}
- private boolean retrieveAccountList() throws IOException, HTTPException {
+ private List<LedgerAccount> retrieveAccountList() throws IOException, HTTPException {
HttpURLConnection http = NetworkUtil.prepareConnection(profile, "accounts");
http.setAllowUserInteraction(false);
switch (http.getResponseCode()) {
case 200:
break;
case 404:
- return false;
+ return null;
default:
throw new HTTPException(http.getResponseCode(), http.getResponseMessage());
}
ArrayList<LedgerAccount> list = new ArrayList<>();
HashMap<String, LedgerAccount> map = new HashMap<>();
HashMap<String, LedgerAccount> currentMap = new HashMap<>();
- for (LedgerAccount acc : Objects.requireNonNull(profile.getAllAccounts()))
+ for (LedgerAccount acc : prevAccounts)
currentMap.put(acc.getName(), acc);
+ throwIfCancelled();
try (InputStream resp = http.getInputStream()) {
+ throwIfCancelled();
if (http.getResponseCode() != 200)
throw new IOException(String.format("HTTP error %d", http.getResponseCode()));
}
}
- profile.setAndStoreAccountListFromWeb(list);
- return true;
+ return list;
}
- private boolean retrieveTransactionList() throws IOException, ParseException, HTTPException {
+ private List<LedgerTransaction> retrieveTransactionList()
+ throws IOException, ParseException, HTTPException {
Progress progress = new Progress();
- int maxTransactionId = Data.transactions.size();
progress.setTotal(expectedPostingsCount);
HttpURLConnection http = NetworkUtil.prepareConnection(profile, "transactions");
case 200:
break;
case 404:
- return false;
+ return null;
default:
throw new HTTPException(http.getResponseCode(), http.getResponseMessage());
}
+ ArrayList<LedgerTransaction> trList = new ArrayList<>();
try (InputStream resp = http.getInputStream()) {
throwIfCancelled();
- ArrayList<LedgerTransaction> trList = new ArrayList<>();
TransactionListParser parser = new TransactionListParser(resp);
progress.setProgress(processedPostings += transaction.getAccounts()
.size());
+// Logger.debug("trParser",
+// String.format(Locale.US, "Parsed transaction %d - %s", transaction
+// .getId(),
+// transaction.getDescription()));
+// for (LedgerTransactionAccount acc : transaction.getAccounts()) {
+// Logger.debug("trParser",
+// String.format(Locale.US, " %s", acc.getAccountName()));
+// }
publishProgress(progress);
}
throwIfCancelled();
- profile.setAndStoreTransactionList(trList);
}
- return true;
+ // json interface returns transactions if file order and the rest of the machinery
+ // expects them in reverse chronological order
+ Collections.sort(trList, (o1, o2) -> {
+ int res = o2.getDate()
+ .compareTo(o1.getDate());
+ if (res != 0)
+ return res;
+ return Integer.compare(o2.getId(), o1.getId());
+ });
+ return trList;
}
@SuppressLint("DefaultLocale")
@Override
- protected String doInBackground(Void... params) {
+ protected Result doInBackground(Void... params) {
Data.backgroundTaskStarted();
+ List<LedgerAccount> accounts;
+ List<LedgerTransaction> transactions;
try {
- if (!retrieveAccountList() || !retrieveTransactionList())
- return retrieveTransactionListLegacy();
- return null;
+ accounts = retrieveAccountList();
+ if (accounts == null)
+ transactions = null;
+ else
+ transactions = retrieveTransactionList();
+ if (accounts == null || transactions == null) {
+ accounts = new ArrayList<>();
+ transactions = new ArrayList<>();
+ retrieveTransactionListLegacy(accounts, transactions);
+ }
+ mainModel.setAndStoreAccountAndTransactionListFromWeb(accounts, transactions);
+
+ return new Result(accounts, transactions);
}
catch (MalformedURLException e) {
e.printStackTrace();
- return "Invalid server URL";
+ return new Result("Invalid server URL");
}
catch (HTTPException e) {
e.printStackTrace();
- return String.format("HTTP error %d: %s", e.getResponseCode(), e.getResponseMessage());
+ return new Result(String.format("HTTP error %d: %s", e.getResponseCode(),
+ e.getResponseMessage()));
}
catch (IOException e) {
e.printStackTrace();
- return e.getLocalizedMessage();
+ return new Result(e.getLocalizedMessage());
}
catch (ParseException e) {
e.printStackTrace();
- return "Network error";
+ return new Result("Network error");
}
catch (OperationCanceledException e) {
e.printStackTrace();
- return "Operation cancelled";
+ return new Result("Operation cancelled");
}
finally {
Data.backgroundTaskFinished();
super(message);
}
}
+
+ public static class Result {
+ public String error;
+ public List<LedgerAccount> accounts;
+ public List<LedgerTransaction> transactions;
+ Result(String error) {
+ this.error = error;
+ }
+ Result(List<LedgerAccount> accounts, List<LedgerTransaction> transactions) {
+ this.accounts = accounts;
+ this.transactions = transactions;
+ }
+ }
}
--- /dev/null
+/*
+ * Copyright © 2020 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
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your opinion), any later version.
+ *
+ * MoLe is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License terms for details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with MoLe. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package net.ktnx.mobileledger.async;
+
+import net.ktnx.mobileledger.model.LedgerTransaction;
+import net.ktnx.mobileledger.model.TransactionListItem;
+import net.ktnx.mobileledger.ui.MainModel;
+import net.ktnx.mobileledger.utils.SimpleDate;
+
+import java.util.ArrayList;
+
+public class TransactionAccumulator {
+ private final ArrayList<TransactionListItem> list = new ArrayList<>();
+ private final MainModel model;
+ private SimpleDate earliestDate, latestDate;
+ private SimpleDate lastDate = SimpleDate.today();
+ private boolean done;
+ public TransactionAccumulator(MainModel model) {
+ this.model = model;
+ }
+ public void put(LedgerTransaction transaction, SimpleDate date) {
+ if (done)
+ throw new IllegalStateException("Can't put new items after done()");
+ if (null == latestDate)
+ latestDate = date;
+ earliestDate = date;
+
+ if (!date.equals(lastDate)) {
+ boolean showMonth = date.month != lastDate.month || date.year != lastDate.year;
+ list.add(new TransactionListItem(date, showMonth));
+ }
+
+ list.add(new TransactionListItem(transaction));
+
+ lastDate = date;
+ }
+ public void done() {
+ done = true;
+ model.setDisplayedTransactions(list);
+ model.setFirstTransactionDate(earliestDate);
+ model.setLastTransactionDate(latestDate);
+ }
+}
import android.os.AsyncTask;
-import net.ktnx.mobileledger.model.Data;
import net.ktnx.mobileledger.model.TransactionListItem;
-import net.ktnx.mobileledger.utils.LockHolder;
+import net.ktnx.mobileledger.ui.MainModel;
import net.ktnx.mobileledger.utils.Logger;
import net.ktnx.mobileledger.utils.SimpleDate;
import java.util.Comparator;
import java.util.List;
import java.util.Locale;
+import java.util.Objects;
-public class TransactionDateFinder extends AsyncTask<SimpleDate, Void, Integer> {
+public class TransactionDateFinder extends AsyncTask<TransactionDateFinder.Params, Void, Integer> {
+ private MainModel model;
@Override
protected void onPostExecute(Integer pos) {
- Data.foundTransactionItemIndex.setValue(pos);
+ model.foundTransactionItemIndex.setValue(pos);
}
@Override
- protected Integer doInBackground(SimpleDate... simpleDates) {
- SimpleDate date = simpleDates[0];
+ protected Integer doInBackground(Params... param) {
+ this.model = param[0].model;
+ SimpleDate date = param[0].date;
Logger.debug("go-to-date",
String.format(Locale.US, "Looking for date %04d-%02d-%02d", date.year, date.month,
date.day));
- Logger.debug("go-to-date", String.format(Locale.US, "List contains %d transactions",
- Data.transactions.size()));
+ List<TransactionListItem> transactions = Objects.requireNonNull(
+ param[0].model.getDisplayedTransactions()
+ .getValue());
+ Logger.debug("go-to-date",
+ String.format(Locale.US, "List contains %d transactions", transactions.size()));
+
+ TransactionListItem target = new TransactionListItem(date, true);
+ int found =
+ Collections.binarySearch(transactions, target, new TransactionListItemComparator());
+ if (found >= 0)
+ return found;
+ else
+ return 1 - found;
+ }
- try (LockHolder locker = Data.transactions.lockForWriting()) {
- List<TransactionListItem> transactions = Data.transactions.getList();
- TransactionListItem target = new TransactionListItem(date, true);
- int found = Collections.binarySearch(transactions, target,
- new TransactionListItemComparator());
- if (found >= 0)
- return found;
- else
- return 1 - found;
+ public static class Params {
+ public MainModel model;
+ public SimpleDate date;
+ public Params(MainModel model, SimpleDate date) {
+ this.model = model;
+ this.date = date;
}
}
+
static class TransactionListItemComparator implements Comparator<TransactionListItem> {
@Override
public int compare(TransactionListItem a, TransactionListItem b) {
import net.ktnx.mobileledger.model.Data;
import net.ktnx.mobileledger.model.LedgerTransaction;
import net.ktnx.mobileledger.model.MobileLedgerProfile;
-import net.ktnx.mobileledger.model.TransactionListItem;
+import net.ktnx.mobileledger.ui.MainModel;
import net.ktnx.mobileledger.utils.SimpleDate;
-import java.util.ArrayList;
-
import static net.ktnx.mobileledger.utils.Logger.debug;
-public class UpdateTransactionsTask extends AsyncTask<String, Void, String> {
- protected String doInBackground(String[] filterAccName) {
+public class UpdateTransactionsTask extends AsyncTask<MainModel, Void, String> {
+ protected String doInBackground(MainModel[] model) {
final MobileLedgerProfile profile = Data.getProfile();
String profile_uuid = profile.getUuid();
Data.backgroundTaskStarted();
try {
- ArrayList<TransactionListItem> newList = new ArrayList<>();
-
String sql;
String[] params;
- if (filterAccName[0] == null) {
+ final String accFilter = model[0].getAccountFilter()
+ .getValue();
+ if (accFilter == null) {
sql = "SELECT id, year, month, day FROM transactions WHERE profile=? ORDER BY " +
"year desc, month desc, day desc, id desc";
params = new String[]{profile_uuid};
"and ta.account_name LIKE ?||'%' AND ta" +
".amount <> 0 ORDER BY tr.year desc, tr.month desc, tr.day desc, tr.id " +
"desc";
- params = new String[]{profile_uuid, filterAccName[0]};
+ params = new String[]{profile_uuid, accFilter};
}
debug("UTT", sql);
- SimpleDate latestDate = null, earliestDate = null;
+ TransactionAccumulator accumulator = new TransactionAccumulator(model[0]);
+
SQLiteDatabase db = App.getDatabase();
- boolean odd = true;
- SimpleDate lastDate = SimpleDate.today();
try (Cursor cursor = db.rawQuery(sql, params)) {
while (cursor.moveToNext()) {
if (isCancelled())
return null;
- int transaction_id = cursor.getInt(0);
- SimpleDate date =
- new SimpleDate(cursor.getInt(1), cursor.getInt(2), cursor.getInt(3));
-
- if (null == latestDate)
- latestDate = date;
- earliestDate = date;
-
- if (!date.equals(lastDate)) {
- boolean showMonth =
- (date.month != lastDate.month) || (date.year != lastDate.year);
- newList.add(new TransactionListItem(date, showMonth));
- }
- newList.add(
- new TransactionListItem(new LedgerTransaction(transaction_id), odd));
-// debug("UTT", String.format("got transaction %d", transaction_id));
-
- lastDate = date;
- odd = !odd;
+ accumulator.put(new LedgerTransaction(cursor.getInt(0)),
+ new SimpleDate(cursor.getInt(1), cursor.getInt(2), cursor.getInt(3)));
}
- Data.transactions.setList(newList);
- Data.latestTransactionDate.postValue(latestDate);
- Data.earliestTransactionDate.postValue(earliestDate);
- debug("UTT", "transaction list value updated");
}
+ accumulator.done();
+ debug("UTT", "transaction list value updated");
+
return null;
}
finally {
tr.addAccount(p.asLedgerAccount());
}
}
+
+ tr.markDataAsLoaded();
return tr;
}
}
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
-import android.os.AsyncTask;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import net.ktnx.mobileledger.utils.Locker;
import net.ktnx.mobileledger.utils.Logger;
import net.ktnx.mobileledger.utils.MLDB;
-import net.ktnx.mobileledger.utils.ObservableList;
-import net.ktnx.mobileledger.utils.SimpleDate;
import java.text.NumberFormat;
import java.util.ArrayList;
-import java.util.Date;
import java.util.List;
import java.util.Locale;
import java.util.Objects;
import static net.ktnx.mobileledger.utils.Logger.debug;
public final class Data {
- public static final ObservableList<TransactionListItem> transactions =
- new ObservableList<>(new ArrayList<>());
- public static final MutableLiveData<SimpleDate> earliestTransactionDate =
- new MutableLiveData<>(null);
- public static final MutableLiveData<SimpleDate> latestTransactionDate =
- new MutableLiveData<>(null);
public static final MutableLiveData<Boolean> backgroundTasksRunning =
new MutableLiveData<>(false);
public static final MutableLiveData<RetrieveTransactionsTask.Progress> backgroundTaskProgress =
new MutableLiveData<>();
- public static final MutableLiveData<Date> lastUpdateDate = new MutableLiveData<>();
public static final MutableLiveData<ArrayList<MobileLedgerProfile>> profiles =
new MutableLiveData<>(null);
- public static final MutableLiveData<String> accountFilter = new MutableLiveData<>();
public static final MutableLiveData<Currency.Position> currencySymbolPosition =
new MutableLiveData<>();
public static final MutableLiveData<Boolean> currencyGap = new MutableLiveData<>(true);
new InertMutableLiveData<>();
private static final AtomicInteger backgroundTaskCount = new AtomicInteger(0);
private static final Locker profilesLocker = new Locker();
- public static MutableLiveData<Integer> foundTransactionItemIndex = new MutableLiveData<>(null);
- private static RetrieveTransactionsTask retrieveTransactionsTask;
static {
locale.setValue(Locale.getDefault());
}
public static void setCurrentProfile(@NonNull MobileLedgerProfile newProfile) {
MLDB.setOption(MLDB.OPT_PROFILE_UUID, newProfile.getUuid());
- stopTransactionsRetrieval();
profile.setValue(newProfile);
}
public static int getProfileIndex(MobileLedgerProfile profile) {
}
return profile;
}
- public synchronized static void scheduleTransactionListRetrieval() {
- if (retrieveTransactionsTask != null) {
- Logger.debug("db", "Ignoring request for transaction retrieval - already active");
- return;
- }
- MobileLedgerProfile pr = profile.getValue();
- if (pr == null) {
- Logger.debug("ui", "Ignoring refresh -- no current profile");
- return;
- }
-
- retrieveTransactionsTask = new RetrieveTransactionsTask(profile.getValue());
- Logger.debug("db", "Created a background transaction retrieval task");
-
- retrieveTransactionsTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
- }
- public static synchronized void stopTransactionsRetrieval() {
- if (retrieveTransactionsTask != null)
- retrieveTransactionsTask.cancel(false);
- }
- public static void transactionRetrievalDone() {
- retrieveTransactionsTask = null;
- }
public static void refreshCurrencyData(Locale locale) {
NumberFormat formatter = NumberFormat.getCurrencyInstance(locale);
java.util.Currency currency = formatter.getCurrency();
import java.text.ParseException;
import java.util.ArrayList;
import java.util.Comparator;
+import java.util.List;
public class LedgerTransaction {
private static final String DIGEST_TYPE = "SHA-256";
private SimpleDate date;
private String description;
private String comment;
- private ArrayList<LedgerTransactionAccount> accounts;
+ private List<LedgerTransactionAccount> accounts;
private String dataHash;
private boolean dataLoaded;
public LedgerTransaction(Integer id, String dateString, String description)
this.dataHash = null;
this.dataLoaded = false;
}
- public ArrayList<LedgerTransactionAccount> getAccounts() {
+ public List<LedgerTransactionAccount> getAccounts() {
return accounts;
}
public void addAccount(LedgerTransactionAccount item) {
}
@NonNull
public SimpleDate getDate() {
- loadData(App.getDatabase());
if (date == null)
throw new IllegalStateException("Transaction has no date");
return date;
return id;
}
protected void fillDataHash() {
+ loadData(App.getDatabase());
if (dataHash != null)
return;
try {
String.format("Unable to get instance of %s digest", DIGEST_TYPE), e);
}
}
- public boolean existsInDb(SQLiteDatabase db) {
- fillDataHash();
- try (Cursor c = db.rawQuery("SELECT 1 from transactions where data_hash = ?",
- new String[]{dataHash}))
- {
- boolean result = c.moveToFirst();
-// debug("db", String.format("Transaction %d (%s) %s", id, dataHash,
-// result ? "already present" : "not present"));
- return result;
- }
- }
- public void loadData(SQLiteDatabase db) {
+ public synchronized void loadData(SQLiteDatabase db) {
if (dataLoaded)
return;
return false;
}
+ public void markDataAsLoaded() {
+ dataLoaded = true;
+ }
}
import android.content.res.Resources;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
-import android.os.Build;
-import android.text.TextUtils;
import android.util.SparseArray;
import androidx.annotation.Nullable;
-import androidx.lifecycle.LiveData;
-import androidx.lifecycle.MutableLiveData;
import net.ktnx.mobileledger.App;
import net.ktnx.mobileledger.R;
import net.ktnx.mobileledger.async.DbOpQueue;
import net.ktnx.mobileledger.async.SendTransactionTask;
-import net.ktnx.mobileledger.utils.LockHolder;
-import net.ktnx.mobileledger.utils.Locker;
import net.ktnx.mobileledger.utils.Logger;
-import net.ktnx.mobileledger.utils.MLDB;
import net.ktnx.mobileledger.utils.Misc;
import net.ktnx.mobileledger.utils.SimpleDate;
import org.jetbrains.annotations.Contract;
import java.util.ArrayList;
-import java.util.Calendar;
-import java.util.Date;
import java.util.HashMap;
-import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import static net.ktnx.mobileledger.utils.Logger.debug;
public final class MobileLedgerProfile {
- private final MutableLiveData<List<LedgerAccount>> displayedAccounts;
- private final MutableLiveData<List<LedgerTransaction>> allTransactions;
- private final MutableLiveData<List<LedgerTransaction>> displayedTransactions;
// N.B. when adding new fields, update the copy-constructor below
private final String uuid;
- private final Locker accountsLocker = new Locker();
- private List<LedgerAccount> allAccounts;
private String name;
private boolean permitPosting;
private boolean showCommentsByDefault;
private int themeHue;
private int orderNo = -1;
private SendTransactionTask.API apiVersion = SendTransactionTask.API.auto;
- private Calendar firstTransactionDate;
- private Calendar lastTransactionDate;
private FutureDates futureDates = FutureDates.None;
private boolean accountsLoaded;
private boolean transactionsLoaded;
// N.B. when adding new fields, update the copy-constructor below
- transient private AccountListLoader loader = null;
- transient private Thread displayedAccountsUpdater;
- transient private AccountListSaver accountListSaver;
- transient private TransactionListSaver transactionListSaver;
transient private AccountAndTransactionListSaver accountAndTransactionListSaver;
- private Map<String, LedgerAccount> accountMap = new HashMap<>();
public MobileLedgerProfile(String uuid) {
this.uuid = uuid;
- allAccounts = new ArrayList<>();
- displayedAccounts = new MutableLiveData<>();
- allTransactions = new MutableLiveData<>(new ArrayList<>());
- displayedTransactions = new MutableLiveData<>(new ArrayList<>());
}
public MobileLedgerProfile(MobileLedgerProfile origin) {
uuid = origin.uuid;
futureDates = origin.futureDates;
apiVersion = origin.apiVersion;
defaultCommodity = origin.defaultCommodity;
- firstTransactionDate = origin.firstTransactionDate;
- lastTransactionDate = origin.lastTransactionDate;
- displayedAccounts = origin.displayedAccounts;
- allAccounts = origin.allAccounts;
- accountMap = origin.accountMap;
- displayedTransactions = origin.displayedTransactions;
- allTransactions = origin.allTransactions;
accountsLoaded = origin.accountsLoaded;
transactionsLoaded = origin.transactionsLoaded;
}
db.endTransaction();
}
}
- public static ArrayList<LedgerAccount> mergeAccountListsFromWeb(List<LedgerAccount> oldList,
- List<LedgerAccount> newList) {
- LedgerAccount oldAcc, newAcc;
- ArrayList<LedgerAccount> merged = new ArrayList<>();
-
- Iterator<LedgerAccount> oldIterator = oldList.iterator();
- Iterator<LedgerAccount> newIterator = newList.iterator();
-
- while (true) {
- if (!oldIterator.hasNext()) {
- // the rest of the incoming are new
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
- newIterator.forEachRemaining(merged::add);
- }
- else {
- while (newIterator.hasNext())
- merged.add(newIterator.next());
- }
- break;
- }
- oldAcc = oldIterator.next();
-
- if (!newIterator.hasNext()) {
- // no more incoming accounts. ignore the rest of the old
- break;
- }
- newAcc = newIterator.next();
-
- // ignore now missing old items
- if (oldAcc.getName()
- .compareTo(newAcc.getName()) < 0)
- continue;
-
- // add newly found items
- if (oldAcc.getName()
- .compareTo(newAcc.getName()) > 0)
- {
- merged.add(newAcc);
- continue;
- }
-
- // two items with same account names; forward-merge UI-controlled fields
- // it is important that the result list contains a new LedgerAccount instance
- // so that the change is propagated to the UI
- newAcc.setExpanded(oldAcc.isExpanded());
- newAcc.setAmountsExpanded(oldAcc.amountsExpanded());
- merged.add(newAcc);
- }
-
- return merged;
- }
- public void mergeAccountListFromWeb(List<LedgerAccount> newList) {
-
- try (LockHolder l = accountsLocker.lockForWriting()) {
- allAccounts = mergeAccountListsFromWeb(allAccounts, newList);
- updateAccountsMap(allAccounts);
- }
- }
- public LiveData<List<LedgerAccount>> getDisplayedAccounts() {
- return displayedAccounts;
- }
@Contract(value = "null -> false", pure = true)
@Override
public boolean equals(@Nullable Object obj) {
return false;
if (apiVersion != p.apiVersion)
return false;
- if (!Objects.equals(firstTransactionDate, p.firstTransactionDate))
- return false;
- if (!Objects.equals(lastTransactionDate, p.lastTransactionDate))
- return false;
return futureDates == p.futureDates;
}
- synchronized public void scheduleAccountListReload() {
- Logger.debug("async-acc", "scheduleAccountListReload() enter");
- if ((loader != null) && loader.isAlive()) {
- Logger.debug("async-acc", "returning early - loader already active");
- return;
- }
-
- Logger.debug("async-acc", "Starting AccountListLoader");
- loader = new AccountListLoader(this);
- loader.start();
- }
- synchronized public void abortAccountListReload() {
- if (loader == null)
- return;
- loader.interrupt();
- loader = null;
- }
public boolean getShowCommentsByDefault() {
return showCommentsByDefault;
}
}
public void storeTransaction(SQLiteDatabase db, int generation, LedgerTransaction tr) {
tr.fillDataHash();
+// Logger.debug("storeTransaction", String.format(Locale.US, "ID %d", tr.getId()));
SimpleDate d = tr.getDate();
db.execSQL("UPDATE transactions SET year=?, month=?, day=?, description=?, comment=?, " +
"data_hash=?, generation=? WHERE profile=? AND id=?",
new Object[]{uuid, generation});
Logger.debug("db/benchmark", "Done deleting obsolete transactions");
}
- private void setLastUpdateStamp() {
- debug("db", "Updating transaction value stamp");
- Date now = new Date();
- setLongOption(MLDB.OPT_LAST_SCRAPE, now.getTime());
- Data.lastUpdateDate.postValue(now);
- }
public void wipeAllData() {
SQLiteDatabase db = App.getDatabase();
db.beginTransaction();
return null;
}
}
- public Calendar getFirstTransactionDate() {
- return firstTransactionDate;
- }
- public Calendar getLastTransactionDate() {
- return lastTransactionDate;
- }
- private void applyTransactionFilter(List<LedgerTransaction> list) {
- final String accFilter = Data.accountFilter.getValue();
- if (TextUtils.isEmpty(accFilter)) {
- displayedTransactions.postValue(list);
- }
- else {
- ArrayList<LedgerTransaction> newList = new ArrayList<>();
- for (LedgerTransaction tr : list) {
- if (tr.hasAccountNamedLike(accFilter))
- newList.add(tr);
- }
- displayedTransactions.postValue(newList);
- }
- }
- synchronized public void storeAccountListAsync(List<LedgerAccount> list,
- boolean storeUiFields) {
- if (accountListSaver != null)
- accountListSaver.interrupt();
- accountListSaver = new AccountListSaver(this, list, storeUiFields);
- accountListSaver.start();
- }
- public void setAndStoreAccountListFromWeb(ArrayList<LedgerAccount> list) {
- SQLiteDatabase db = App.getDatabase();
- db.beginTransactionNonExclusive();
- try {
- Logger.debug("db/benchmark",
- String.format(Locale.US, "Storing %d accounts", list.size()));
- int gen = getNextAccountsGeneration(db);
- Logger.debug("db/benckmark",
- String.format(Locale.US, "Got next generation of %d", gen));
- for (LedgerAccount acc : list) {
- storeAccount(db, gen, acc, false);
- for (LedgerAmount amt : acc.getAmounts()) {
- storeAccountValue(db, gen, acc.getName(), amt.getCurrency(), amt.getAmount());
- }
- }
- Logger.debug("db/benchmark", "Done storing accounts");
- deleteNotPresentAccounts(db, gen);
- setLastUpdateStamp();
- db.setTransactionSuccessful();
- }
- finally {
- db.endTransaction();
- }
-
- mergeAccountListFromWeb(list);
- updateDisplayedAccounts();
- }
- public synchronized Locker lockAccountsForWriting() {
- accountsLocker.lockForWriting();
- return accountsLocker;
- }
- public void setAndStoreTransactionList(ArrayList<LedgerTransaction> list) {
- storeTransactionListAsync(this, list);
-
- allTransactions.postValue(list);
- }
- private void storeTransactionListAsync(MobileLedgerProfile mobileLedgerProfile,
- List<LedgerTransaction> list) {
- if (transactionListSaver != null)
- transactionListSaver.interrupt();
-
- transactionListSaver = new TransactionListSaver(this, list);
- transactionListSaver.start();
- }
- public void setAndStoreAccountAndTransactionListFromWeb(List<LedgerAccount> accounts,
- List<LedgerTransaction> transactions) {
- storeAccountAndTransactionListAsync(accounts, transactions, false);
-
- mergeAccountListFromWeb(accounts);
- updateDisplayedAccounts();
-
- allTransactions.postValue(transactions);
- }
- private void storeAccountAndTransactionListAsync(List<LedgerAccount> accounts,
- List<LedgerTransaction> transactions,
- boolean storeAccUiFields) {
+ public void storeAccountAndTransactionListAsync(List<LedgerAccount> accounts,
+ List<LedgerTransaction> transactions) {
if (accountAndTransactionListSaver != null)
accountAndTransactionListSaver.interrupt();
accountAndTransactionListSaver =
- new AccountAndTransactionListSaver(this, accounts, transactions, storeAccUiFields);
+ new AccountAndTransactionListSaver(this, accounts, transactions);
accountAndTransactionListSaver.start();
}
- synchronized public void updateDisplayedAccounts() {
- if (displayedAccountsUpdater != null) {
- displayedAccountsUpdater.interrupt();
- }
- displayedAccountsUpdater = new AccountListDisplayedFilter(this, allAccounts);
- displayedAccountsUpdater.start();
- }
- public List<LedgerAccount> getAllAccounts() {
- return allAccounts;
- }
- private void updateAccountsMap(List<LedgerAccount> newAccounts) {
- accountMap.clear();
- for (LedgerAccount acc : newAccounts) {
- accountMap.put(acc.getName(), acc);
- }
- }
- @Nullable
- public LedgerAccount locateAccount(String name) {
- return accountMap.get(name);
- }
public enum FutureDates {
None(0), OneWeek(7), TwoWeeks(14), OneMonth(30), TwoMonths(60), ThreeMonths(90),
}
}
- static class AccountListLoader extends Thread {
- MobileLedgerProfile profile;
- AccountListLoader(MobileLedgerProfile profile) {
- this.profile = profile;
- }
- @Override
- public void run() {
- Logger.debug("async-acc", "AccountListLoader::run() entered");
- String profileUUID = profile.getUuid();
- ArrayList<LedgerAccount> list = new ArrayList<>();
- HashMap<String, LedgerAccount> map = new HashMap<>();
-
- String sql = "SELECT a.name, a.expanded, a.amounts_expanded";
- sql += " from accounts a WHERE a.profile = ?";
- sql += " ORDER BY a.name";
-
- SQLiteDatabase db = App.getDatabase();
- Logger.debug("async-acc", "AccountListLoader::run() connected to DB");
- try (Cursor cursor = db.rawQuery(sql, new String[]{profileUUID})) {
- Logger.debug("async-acc", "AccountListLoader::run() executed query");
- while (cursor.moveToNext()) {
- if (isInterrupted())
- return;
-
- final String accName = cursor.getString(0);
-// debug("accounts",
-// String.format("Read account '%s' from DB [%s]", accName,
-// profileUUID));
- String parentName = LedgerAccount.extractParentName(accName);
- LedgerAccount parent;
- if (parentName != null) {
- parent = map.get(parentName);
- if (parent == null)
- throw new IllegalStateException(
- String.format("Can't load account '%s': parent '%s' not loaded",
- accName, parentName));
- parent.setHasSubAccounts(true);
- }
- else
- parent = null;
-
- LedgerAccount acc = new LedgerAccount(profile, accName, parent);
- acc.setExpanded(cursor.getInt(1) == 1);
- acc.setAmountsExpanded(cursor.getInt(2) == 1);
- acc.setHasSubAccounts(false);
-
- try (Cursor c2 = db.rawQuery(
- "SELECT value, currency FROM account_values WHERE profile = ?" + " " +
- "AND account = ?", new String[]{profileUUID, accName}))
- {
- while (c2.moveToNext()) {
- acc.addAmount(c2.getFloat(0), c2.getString(1));
- }
- }
-
- list.add(acc);
- map.put(accName, acc);
- }
- Logger.debug("async-acc", "AccountListLoader::run() query execution done");
- }
-
- if (isInterrupted())
- return;
-
- Logger.debug("async-acc", "AccountListLoader::run() posting new list");
- profile.allAccounts = list;
- profile.updateAccountsMap(list);
- profile.updateDisplayedAccounts();
- }
- }
-
- static class AccountListDisplayedFilter extends Thread {
- private final MobileLedgerProfile profile;
- private final List<LedgerAccount> list;
- AccountListDisplayedFilter(MobileLedgerProfile profile, List<LedgerAccount> list) {
- this.profile = profile;
- this.list = list;
- }
- @Override
- public void run() {
- List<LedgerAccount> newDisplayed = new ArrayList<>();
- Logger.debug("dFilter", "waiting for synchronized block");
- Logger.debug("dFilter", String.format(Locale.US,
- "entered synchronized block (about to examine %d accounts)", list.size()));
- for (LedgerAccount a : list) {
- if (isInterrupted()) {
- return;
- }
-
- if (a.isVisible()) {
- newDisplayed.add(a);
- }
- }
- if (!isInterrupted()) {
- profile.displayedAccounts.postValue(newDisplayed);
- }
- Logger.debug("dFilter", "left synchronized block");
- }
- }
-
- private static class AccountListSaver extends Thread {
- private final MobileLedgerProfile profile;
- private final List<LedgerAccount> list;
- private final boolean storeUiFields;
- AccountListSaver(MobileLedgerProfile profile, List<LedgerAccount> list,
- boolean storeUiFields) {
- this.list = list;
- this.profile = profile;
- this.storeUiFields = storeUiFields;
- }
- @Override
- public void run() {
- SQLiteDatabase db = App.getDatabase();
- db.beginTransactionNonExclusive();
- try {
- int generation = profile.getNextAccountsGeneration(db);
- if (isInterrupted())
- return;
- for (LedgerAccount acc : list) {
- profile.storeAccount(db, generation, acc, storeUiFields);
- if (isInterrupted())
- return;
- }
- profile.deleteNotPresentAccounts(db, generation);
- if (isInterrupted())
- return;
- profile.setLastUpdateStamp();
- db.setTransactionSuccessful();
- }
- finally {
- db.endTransaction();
- }
- }
- }
-
- private static class TransactionListSaver extends Thread {
- private final MobileLedgerProfile profile;
- private final List<LedgerTransaction> list;
- TransactionListSaver(MobileLedgerProfile profile, List<LedgerTransaction> list) {
- this.list = list;
- this.profile = profile;
- }
- @Override
- public void run() {
- SQLiteDatabase db = App.getDatabase();
- db.beginTransactionNonExclusive();
- try {
- Logger.debug("db/benchmark",
- String.format(Locale.US, "Storing %d transactions", list.size()));
- int generation = profile.getNextTransactionsGeneration(db);
- Logger.debug("db/benchmark",
- String.format(Locale.US, "Got next generation of %d", generation));
- if (isInterrupted())
- return;
- for (LedgerTransaction tr : list) {
- profile.storeTransaction(db, generation, tr);
- if (isInterrupted())
- return;
- }
- Logger.debug("db/benchmark", "Done storing transactions");
- profile.deleteNotPresentTransactions(db, generation);
- if (isInterrupted())
- return;
- profile.setLastUpdateStamp();
- db.setTransactionSuccessful();
- }
- finally {
- db.endTransaction();
- }
- }
- }
-
private static class AccountAndTransactionListSaver extends Thread {
private final MobileLedgerProfile profile;
private final List<LedgerAccount> accounts;
private final List<LedgerTransaction> transactions;
- private final boolean storeAccUiFields;
AccountAndTransactionListSaver(MobileLedgerProfile profile, List<LedgerAccount> accounts,
- List<LedgerTransaction> transactions,
- boolean storeAccUiFields) {
+ List<LedgerTransaction> transactions) {
this.accounts = accounts;
this.transactions = transactions;
this.profile = profile;
- this.storeAccUiFields = storeAccUiFields;
+ }
+ public int getNextDescriptionsGeneration(SQLiteDatabase db) {
+ int generation = 1;
+ try (Cursor c = db.rawQuery("SELECT generation FROM description_history LIMIT 1",
+ null))
+ {
+ if (c.moveToFirst()) {
+ generation = c.getInt(0) + 1;
+ }
+ }
+ return generation;
+ }
+ void deleteNotPresentDescriptions(SQLiteDatabase db, int generation) {
+ Logger.debug("db/benchmark", "Deleting obsolete descriptions");
+ db.execSQL("DELETE FROM description_history WHERE generation <> ?",
+ new Object[]{generation});
+ db.execSQL("DELETE FROM description_history WHERE generation <> ?",
+ new Object[]{generation});
+ Logger.debug("db/benchmark", "Done deleting obsolete descriptions");
}
@Override
public void run() {
return;
int transactionsGeneration = profile.getNextTransactionsGeneration(db);
- if (isInterrupted()) {
+ if (isInterrupted())
return;
- }
for (LedgerAccount acc : accounts) {
- profile.storeAccount(db, accountsGeneration, acc, storeAccUiFields);
+ profile.storeAccount(db, accountsGeneration, acc, false);
if (isInterrupted())
return;
+ for (LedgerAmount amt : acc.getAmounts()) {
+ profile.storeAccountValue(db, accountsGeneration, acc.getName(),
+ amt.getCurrency(), amt.getAmount());
+ if (isInterrupted())
+ return;
+ }
}
for (LedgerTransaction tr : transactions) {
profile.storeTransaction(db, transactionsGeneration, tr);
- if (isInterrupted()) {
+ if (isInterrupted())
return;
- }
}
- profile.deleteNotPresentAccounts(db, accountsGeneration);
+ profile.deleteNotPresentTransactions(db, transactionsGeneration);
if (isInterrupted()) {
return;
}
- profile.deleteNotPresentTransactions(db, transactionsGeneration);
+ profile.deleteNotPresentAccounts(db, accountsGeneration);
if (isInterrupted())
return;
- profile.setLastUpdateStamp();
+ Map<String, Boolean> unique = new HashMap<>();
+
+ debug("descriptions", "Starting refresh");
+ int descriptionsGeneration = getNextDescriptionsGeneration(db);
+ try (Cursor c = db.rawQuery("SELECT distinct description from transactions",
+ null))
+ {
+ while (c.moveToNext()) {
+ String description = c.getString(0);
+ String descriptionUpper = description.toUpperCase();
+ if (unique.containsKey(descriptionUpper))
+ continue;
+
+ storeDescription(db, descriptionsGeneration, description, descriptionUpper);
+
+ unique.put(descriptionUpper, true);
+ }
+ }
+ deleteNotPresentDescriptions(db, descriptionsGeneration);
db.setTransactionSuccessful();
}
db.endTransaction();
}
}
+ private void storeDescription(SQLiteDatabase db, int generation, String description,
+ String descriptionUpper) {
+ db.execSQL("UPDATE description_history SET description=?, generation=? WHERE " +
+ "description_upper=?", new Object[]{description, generation, descriptionUpper
+ });
+ db.execSQL(
+ "INSERT INTO description_history(description, description_upper, generation) " +
+ "select ?,?,? WHERE (select changes() = 0)",
+ new Object[]{description, descriptionUpper, generation
+ });
+ }
}
}
import androidx.annotation.NonNull;
+import net.ktnx.mobileledger.App;
import net.ktnx.mobileledger.utils.SimpleDate;
public class TransactionListItem {
private SimpleDate date;
private boolean monthShown;
private LedgerTransaction transaction;
- private boolean odd;
public TransactionListItem(SimpleDate date, boolean monthShown) {
this.type = Type.DELIMITER;
this.date = date;
this.monthShown = monthShown;
}
- public TransactionListItem(LedgerTransaction transaction, boolean isOdd) {
+ public TransactionListItem(LedgerTransaction transaction) {
this.type = Type.TRANSACTION;
this.transaction = transaction;
- this.odd = isOdd;
}
@NonNull
public Type getType() {
}
@NonNull
public SimpleDate getDate() {
- return (date != null) ? date : transaction.getDate();
+ if (date != null)
+ return date;
+ transaction.loadData(App.getDatabase());
+ return transaction.getDate();
}
public boolean isMonthShown() {
return monthShown;
public LedgerTransaction getTransaction() {
return transaction;
}
- public boolean isOdd() {
- return odd;
- }
public enum Type {TRANSACTION, DELIMITER}
}
--- /dev/null
+/*
+ * Copyright © 2020 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
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your opinion), any later version.
+ *
+ * MoLe is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License terms for details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with MoLe. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package net.ktnx.mobileledger.ui;
+
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+import android.os.AsyncTask;
+import android.os.Build;
+import android.text.TextUtils;
+
+import androidx.annotation.Nullable;
+import androidx.lifecycle.LiveData;
+import androidx.lifecycle.MutableLiveData;
+import androidx.lifecycle.ViewModel;
+
+import net.ktnx.mobileledger.App;
+import net.ktnx.mobileledger.async.RetrieveTransactionsTask;
+import net.ktnx.mobileledger.async.TransactionAccumulator;
+import net.ktnx.mobileledger.async.UpdateTransactionsTask;
+import net.ktnx.mobileledger.model.Data;
+import net.ktnx.mobileledger.model.LedgerAccount;
+import net.ktnx.mobileledger.model.LedgerTransaction;
+import net.ktnx.mobileledger.model.MobileLedgerProfile;
+import net.ktnx.mobileledger.model.TransactionListItem;
+import net.ktnx.mobileledger.utils.LockHolder;
+import net.ktnx.mobileledger.utils.Locker;
+import net.ktnx.mobileledger.utils.Logger;
+import net.ktnx.mobileledger.utils.MLDB;
+import net.ktnx.mobileledger.utils.SimpleDate;
+
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+
+import static net.ktnx.mobileledger.utils.Logger.debug;
+
+public class MainModel extends ViewModel {
+ public final MutableLiveData<Integer> foundTransactionItemIndex = new MutableLiveData<>(null);
+ public final MutableLiveData<Date> lastUpdateDate = new MutableLiveData<>(null);
+ private final MutableLiveData<Boolean> updatingFlag = new MutableLiveData<>(false);
+ private final MutableLiveData<String> accountFilter = new MutableLiveData<>();
+ private final MutableLiveData<List<TransactionListItem>> displayedTransactions =
+ new MutableLiveData<>(new ArrayList<>());
+ private final MutableLiveData<List<LedgerAccount>> displayedAccounts = new MutableLiveData<>();
+ private final Locker accountsLocker = new Locker();
+ private final MutableLiveData<String> updateError = new MutableLiveData<>();
+ private MobileLedgerProfile profile;
+ private List<LedgerAccount> allAccounts = new ArrayList<>();
+ private Map<String, LedgerAccount> accountMap = new HashMap<>();
+ private SimpleDate firstTransactionDate;
+ private SimpleDate lastTransactionDate;
+ transient private RetrieveTransactionsTask retrieveTransactionsTask;
+ transient private Thread displayedAccountsUpdater;
+ transient private AccountListLoader loader = null;
+ private TransactionsDisplayedFilter displayedTransactionsUpdater;
+ public static ArrayList<LedgerAccount> mergeAccountListsFromWeb(List<LedgerAccount> oldList,
+ List<LedgerAccount> newList) {
+ LedgerAccount oldAcc, newAcc;
+ ArrayList<LedgerAccount> merged = new ArrayList<>();
+
+ Iterator<LedgerAccount> oldIterator = oldList.iterator();
+ Iterator<LedgerAccount> newIterator = newList.iterator();
+
+ while (true) {
+ if (!oldIterator.hasNext()) {
+ // the rest of the incoming are new
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
+ newIterator.forEachRemaining(merged::add);
+ }
+ else {
+ while (newIterator.hasNext())
+ merged.add(newIterator.next());
+ }
+ break;
+ }
+ oldAcc = oldIterator.next();
+
+ if (!newIterator.hasNext()) {
+ // no more incoming accounts. ignore the rest of the old
+ break;
+ }
+ newAcc = newIterator.next();
+
+ // ignore now missing old items
+ if (oldAcc.getName()
+ .compareTo(newAcc.getName()) < 0)
+ continue;
+
+ // add newly found items
+ if (oldAcc.getName()
+ .compareTo(newAcc.getName()) > 0)
+ {
+ merged.add(newAcc);
+ continue;
+ }
+
+ // two items with same account names; forward-merge UI-controlled fields
+ // it is important that the result list contains a new LedgerAccount instance
+ // so that the change is propagated to the UI
+ newAcc.setExpanded(oldAcc.isExpanded());
+ newAcc.setAmountsExpanded(oldAcc.amountsExpanded());
+ merged.add(newAcc);
+ }
+
+ return merged;
+ }
+ private void setLastUpdateStamp() {
+ debug("db", "Updating transaction value stamp");
+ Date now = new Date();
+ profile.setLongOption(MLDB.OPT_LAST_SCRAPE, now.getTime());
+ lastUpdateDate.postValue(now);
+ }
+ public void scheduleTransactionListReload() {
+ UpdateTransactionsTask task = new UpdateTransactionsTask();
+ task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, this);
+ }
+ public LiveData<Boolean> getUpdatingFlag() {
+ return updatingFlag;
+ }
+ public LiveData<String> getUpdateError() {
+ return updateError;
+ }
+ public void setProfile(MobileLedgerProfile profile) {
+ stopTransactionsRetrieval();
+ this.profile = profile;
+ }
+ public LiveData<List<TransactionListItem>> getDisplayedTransactions() {
+ return displayedTransactions;
+ }
+ public void setDisplayedTransactions(List<TransactionListItem> list) {
+ displayedTransactions.postValue(list);
+ }
+ public SimpleDate getFirstTransactionDate() {
+ return firstTransactionDate;
+ }
+ public void setFirstTransactionDate(SimpleDate earliestDate) {
+ this.firstTransactionDate = earliestDate;
+ }
+ public MutableLiveData<String> getAccountFilter() {
+ return accountFilter;
+ }
+ public SimpleDate getLastTransactionDate() {
+ return lastTransactionDate;
+ }
+ public void setLastTransactionDate(SimpleDate latestDate) {
+ this.lastTransactionDate = latestDate;
+ }
+ private void applyTransactionFilter(List<LedgerTransaction> list) {
+ final String accFilter = accountFilter.getValue();
+ ArrayList<TransactionListItem> newList = new ArrayList<>();
+
+ TransactionAccumulator accumulator = new TransactionAccumulator(this);
+ if (TextUtils.isEmpty(accFilter))
+ for (LedgerTransaction tr : list)
+ newList.add(new TransactionListItem(tr));
+ else
+ for (LedgerTransaction tr : list)
+ if (tr.hasAccountNamedLike(accFilter))
+ newList.add(new TransactionListItem(tr));
+
+ displayedTransactions.postValue(newList);
+ }
+ public synchronized void scheduleTransactionListRetrieval() {
+ if (retrieveTransactionsTask != null) {
+ Logger.debug("db", "Ignoring request for transaction retrieval - already active");
+ return;
+ }
+ MobileLedgerProfile profile = Data.getProfile();
+
+ retrieveTransactionsTask = new RetrieveTransactionsTask(this, profile, allAccounts);
+ Logger.debug("db", "Created a background transaction retrieval task");
+
+ retrieveTransactionsTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
+ }
+ public synchronized void stopTransactionsRetrieval() {
+ if (retrieveTransactionsTask != null)
+ retrieveTransactionsTask.cancel(true);
+ }
+ public void transactionRetrievalDone() {
+ retrieveTransactionsTask = null;
+ }
+ public synchronized Locker lockAccountsForWriting() {
+ accountsLocker.lockForWriting();
+ return accountsLocker;
+ }
+ public void mergeAccountListFromWeb(List<LedgerAccount> newList) {
+
+ try (LockHolder l = accountsLocker.lockForWriting()) {
+ allAccounts = mergeAccountListsFromWeb(allAccounts, newList);
+ updateAccountsMap(allAccounts);
+ }
+ }
+ public LiveData<List<LedgerAccount>> getDisplayedAccounts() {
+ return displayedAccounts;
+ }
+ synchronized public void scheduleAccountListReload() {
+ Logger.debug("async-acc", "scheduleAccountListReload() enter");
+ if ((loader != null) && loader.isAlive()) {
+ Logger.debug("async-acc", "returning early - loader already active");
+ return;
+ }
+
+ loader = new AccountListLoader(profile, this);
+ loader.start();
+ }
+ public synchronized void setAndStoreAccountAndTransactionListFromWeb(
+ List<LedgerAccount> accounts, List<LedgerTransaction> transactions) {
+ profile.storeAccountAndTransactionListAsync(accounts, transactions);
+
+ setLastUpdateStamp();
+
+ mergeAccountListFromWeb(accounts);
+ updateDisplayedAccounts();
+
+ updateDisplayedTransactionsFromWeb(transactions);
+ }
+ synchronized public void abortAccountListReload() {
+ if (loader == null)
+ return;
+ loader.interrupt();
+ loader = null;
+ }
+ synchronized public void updateDisplayedAccounts() {
+ if (displayedAccountsUpdater != null) {
+ displayedAccountsUpdater.interrupt();
+ }
+ displayedAccountsUpdater = new AccountListDisplayedFilter(this, allAccounts);
+ displayedAccountsUpdater.start();
+ }
+ synchronized public void updateDisplayedTransactionsFromWeb(List<LedgerTransaction> list) {
+ if (displayedTransactionsUpdater != null) {
+ displayedTransactionsUpdater.interrupt();
+ }
+ displayedTransactionsUpdater = new TransactionsDisplayedFilter(this, list);
+ displayedTransactionsUpdater.start();
+ }
+ public List<LedgerAccount> getAllAccounts() {
+ return allAccounts;
+ }
+ private void updateAccountsMap(List<LedgerAccount> newAccounts) {
+ accountMap.clear();
+ for (LedgerAccount acc : newAccounts) {
+ accountMap.put(acc.getName(), acc);
+ }
+ }
+ @Nullable
+ public LedgerAccount locateAccount(String name) {
+ return accountMap.get(name);
+ }
+ public void clearUpdateError() {
+ updateError.postValue(null);
+ }
+ public void clearTransactions() {
+ displayedTransactions.setValue(new ArrayList<>());
+ }
+
+ static class AccountListLoader extends Thread {
+ private MobileLedgerProfile profile;
+ private MainModel model;
+ AccountListLoader(MobileLedgerProfile profile, MainModel model) {
+ this.profile = profile;
+ this.model = model;
+ }
+ @Override
+ public void run() {
+ Logger.debug("async-acc", "AccountListLoader::run() entered");
+ String profileUUID = profile.getUuid();
+ ArrayList<LedgerAccount> list = new ArrayList<>();
+ HashMap<String, LedgerAccount> map = new HashMap<>();
+
+ String sql = "SELECT a.name, a.expanded, a.amounts_expanded";
+ sql += " from accounts a WHERE a.profile = ?";
+ sql += " ORDER BY a.name";
+
+ SQLiteDatabase db = App.getDatabase();
+ Logger.debug("async-acc", "AccountListLoader::run() connected to DB");
+ try (Cursor cursor = db.rawQuery(sql, new String[]{profileUUID})) {
+ Logger.debug("async-acc", "AccountListLoader::run() executed query");
+ while (cursor.moveToNext()) {
+ if (isInterrupted())
+ return;
+
+ final String accName = cursor.getString(0);
+// debug("accounts",
+// String.format("Read account '%s' from DB [%s]", accName,
+// profileUUID));
+ String parentName = LedgerAccount.extractParentName(accName);
+ LedgerAccount parent;
+ if (parentName != null) {
+ parent = map.get(parentName);
+ if (parent == null)
+ throw new IllegalStateException(
+ String.format("Can't load account '%s': parent '%s' not loaded",
+ accName, parentName));
+ parent.setHasSubAccounts(true);
+ }
+ else
+ parent = null;
+
+ LedgerAccount acc = new LedgerAccount(profile, accName, parent);
+ acc.setExpanded(cursor.getInt(1) == 1);
+ acc.setAmountsExpanded(cursor.getInt(2) == 1);
+ acc.setHasSubAccounts(false);
+
+ try (Cursor c2 = db.rawQuery(
+ "SELECT value, currency FROM account_values WHERE profile = ?" + " " +
+ "AND account = ?", new String[]{profileUUID, accName}))
+ {
+ while (c2.moveToNext()) {
+ acc.addAmount(c2.getFloat(0), c2.getString(1));
+ }
+ }
+
+ list.add(acc);
+ map.put(accName, acc);
+ }
+ Logger.debug("async-acc", "AccountListLoader::run() query execution done");
+ }
+
+ if (isInterrupted())
+ return;
+
+ Logger.debug("async-acc", "AccountListLoader::run() posting new list");
+ model.allAccounts = list;
+ model.updateAccountsMap(list);
+ model.updateDisplayedAccounts();
+ }
+ }
+
+ static class AccountListDisplayedFilter extends Thread {
+ private final MainModel model;
+ private final List<LedgerAccount> list;
+ AccountListDisplayedFilter(MainModel model, List<LedgerAccount> list) {
+ this.model = model;
+ this.list = list;
+ }
+ @Override
+ public void run() {
+ List<LedgerAccount> newDisplayed = new ArrayList<>();
+ Logger.debug("dFilter", "waiting for synchronized block");
+ Logger.debug("dFilter", String.format(Locale.US,
+ "entered synchronized block (about to examine %d accounts)", list.size()));
+ for (LedgerAccount a : list) {
+ if (isInterrupted()) {
+ return;
+ }
+
+ if (a.isVisible()) {
+ newDisplayed.add(a);
+ }
+ }
+ if (!isInterrupted()) {
+ model.displayedAccounts.postValue(newDisplayed);
+ }
+ Logger.debug("dFilter", "left synchronized block");
+ }
+ }
+
+ static class TransactionsDisplayedFilter extends Thread {
+ private final MainModel model;
+ private final List<LedgerTransaction> list;
+ TransactionsDisplayedFilter(MainModel model, List<LedgerTransaction> list) {
+ this.model = model;
+ this.list = list;
+ }
+ @Override
+ public void run() {
+ List<LedgerAccount> newDisplayed = new ArrayList<>();
+ Logger.debug("dFilter", "waiting for synchronized block");
+ Logger.debug("dFilter", String.format(Locale.US,
+ "entered synchronized block (about to examine %d transactions)", list.size()));
+ String accNameFilter = model.getAccountFilter()
+ .getValue();
+
+ TransactionAccumulator acc = new TransactionAccumulator(model);
+ for (LedgerTransaction tr : list) {
+ if (isInterrupted()) {
+ return;
+ }
+
+ if (accNameFilter == null || tr.hasAccountNamedLike(accNameFilter)) {
+ acc.put(tr, tr.getDate());
+ }
+ }
+ if (!isInterrupted()) {
+ acc.done();
+ }
+ Logger.debug("dFilter", "left synchronized block");
+ }
+ }
+}
import net.ktnx.mobileledger.async.DbOpQueue;
import net.ktnx.mobileledger.model.LedgerAccount;
import net.ktnx.mobileledger.model.MobileLedgerProfile;
+import net.ktnx.mobileledger.ui.MainModel;
import net.ktnx.mobileledger.ui.activity.MainActivity;
import net.ktnx.mobileledger.utils.Locker;
import net.ktnx.mobileledger.utils.Logger;
extends RecyclerView.Adapter<AccountSummaryAdapter.LedgerRowHolder> {
public static final int AMOUNT_LIMIT = 3;
private AsyncListDiffer<LedgerAccount> listDiffer;
- AccountSummaryAdapter() {
+ private MainModel model;
+ AccountSummaryAdapter(MainModel model) {
+ this.model = model;
+
listDiffer = new AsyncListDiffer<>(this, new DiffUtil.ItemCallback<LedgerAccount>() {
@Override
public boolean areItemsTheSame(@NotNull LedgerAccount oldItem,
public void setAccounts(List<LedgerAccount> newList) {
listDiffer.submitList(newList);
}
- static class LedgerRowHolder extends RecyclerView.ViewHolder {
+ class LedgerRowHolder extends RecyclerView.ViewHolder {
TextView tvAccountName, tvAccountAmounts;
ConstraintLayout row;
View expanderContainer;
if (profile == null) {
return;
}
- try (Locker ignored = profile.lockAccountsForWriting()) {
- LedgerAccount realAccount = profile.locateAccount(mAccount.getName());
+ try (Locker ignored = model.lockAccountsForWriting()) {
+ LedgerAccount realAccount = model.locateAccount(mAccount.getName());
if (realAccount == null)
return;
}
expanderContainer.animate()
.rotation(mAccount.isExpanded() ? 0 : 180);
- profile.updateDisplayedAccounts();
+ model.updateDisplayedAccounts();
DbOpQueue.add("update accounts set expanded=? where name=? and profile=?",
new Object[]{mAccount.isExpanded(), mAccount.getName(), profile.getUuid()
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
+import androidx.lifecycle.ViewModelProvider;
import androidx.recyclerview.widget.DividerItemDecoration;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import net.ktnx.mobileledger.R;
import net.ktnx.mobileledger.model.Data;
import net.ktnx.mobileledger.model.LedgerAccount;
-import net.ktnx.mobileledger.model.MobileLedgerProfile;
+import net.ktnx.mobileledger.ui.MainModel;
import net.ktnx.mobileledger.ui.MobileLedgerListFragment;
import net.ktnx.mobileledger.ui.activity.MainActivity;
import net.ktnx.mobileledger.utils.Colors;
debug("flow", "AccountSummaryFragment.onActivityCreated()");
super.onActivityCreated(savedInstanceState);
+ MainModel model = new ViewModelProvider(requireActivity()).get(MainModel.class);
+
Data.backgroundTasksRunning.observe(this.getViewLifecycleOwner(),
this::onBackgroundTaskRunningChanged);
- modelAdapter = new AccountSummaryAdapter();
+ modelAdapter = new AccountSummaryAdapter(model);
MainActivity mainActivity = getMainActivity();
root = mainActivity.findViewById(R.id.account_root);
Colors.themeWatch.observe(getViewLifecycleOwner(), this::themeChanged);
refreshLayout.setOnRefreshListener(() -> {
debug("ui", "refreshing accounts via swipe");
- Data.scheduleTransactionListRetrieval();
+ model.scheduleTransactionListRetrieval();
});
- MobileLedgerProfile profile = Data.getProfile();
- profile.getDisplayedAccounts()
- .observe(getViewLifecycleOwner(), this::onAccountsChanged);
+ model.getDisplayedAccounts()
+ .observe(getViewLifecycleOwner(), this::onAccountsChanged);
}
private void onAccountsChanged(List<LedgerAccount> accounts) {
Logger.debug("async-acc",
import android.content.res.ColorStateList;
import android.graphics.Color;
import android.graphics.drawable.Icon;
-import android.os.AsyncTask;
import android.os.Build;
import android.os.Bundle;
import android.util.Log;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentPagerAdapter;
+import androidx.lifecycle.ViewModelProvider;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import androidx.viewpager.widget.ViewPager;
import com.google.android.material.snackbar.Snackbar;
import net.ktnx.mobileledger.R;
-import net.ktnx.mobileledger.async.RefreshDescriptionsTask;
import net.ktnx.mobileledger.async.RetrieveTransactionsTask;
import net.ktnx.mobileledger.model.Data;
import net.ktnx.mobileledger.model.MobileLedgerProfile;
+import net.ktnx.mobileledger.ui.MainModel;
import net.ktnx.mobileledger.ui.account_summary.AccountSummaryFragment;
import net.ktnx.mobileledger.ui.profiles.ProfileDetailFragment;
import net.ktnx.mobileledger.ui.profiles.ProfilesRecyclerViewAdapter;
import net.ktnx.mobileledger.ui.transaction_list.TransactionListFragment;
-import net.ktnx.mobileledger.ui.transaction_list.TransactionListViewModel;
import net.ktnx.mobileledger.utils.Colors;
import net.ktnx.mobileledger.utils.Logger;
import net.ktnx.mobileledger.utils.MLDB;
private ActionBarDrawerToggle barDrawerToggle;
private ViewPager.SimpleOnPageChangeListener pageChangeListener;
private MobileLedgerProfile profile;
+ private MainModel mainModel;
@Override
protected void onStart() {
super.onStart();
protected void onSaveInstanceState(@NotNull Bundle outState) {
super.onSaveInstanceState(outState);
outState.putInt(STATE_CURRENT_PAGE, mViewPager.getCurrentItem());
- if (Data.accountFilter.getValue() != null)
- outState.putString(STATE_ACC_FILTER, Data.accountFilter.getValue());
+ if (mainModel.getAccountFilter()
+ .getValue() != null)
+ outState.putString(STATE_ACC_FILTER, mainModel.getAccountFilter()
+ .getValue());
}
@Override
protected void onDestroy() {
Logger.debug("MainActivity", "onCreate()/after super");
setContentView(R.layout.activity_main);
+ mainModel = new ViewModelProvider(this).get(MainModel.class);
+
fab = findViewById(R.id.btn_add_transaction);
profileListHeadMore = findViewById(R.id.nav_profiles_start_edit);
profileListHeadCancel = findViewById(R.id.nav_profiles_cancel_edit);
if (currentPage != -1) {
mCurrentPage = currentPage;
}
- Data.accountFilter.setValue(savedInstanceState.getString(STATE_ACC_FILTER, null));
+ mainModel.getAccountFilter()
+ .setValue(savedInstanceState.getString(STATE_ACC_FILTER, null));
}
- Data.lastUpdateDate.observe(this, this::updateLastUpdateDisplay);
+ mainModel.lastUpdateDate.observe(this, this::updateLastUpdateDisplay);
findViewById(R.id.btn_no_profiles_add).setOnClickListener(
v -> startEditProfileActivity(null));
else
drawer.close();
});
+
+ mainModel.getUpdateError()
+ .observe(this, (error) -> {
+ if (error == null)
+ return;
+
+ Snackbar.make(mViewPager, error, Snackbar.LENGTH_LONG)
+ .show();
+ mainModel.clearUpdateError();
+ });
}
private void scheduleDataRetrievalIfStale(Date lastUpdate) {
long now = new Date().getTime();
"WEB data last fetched at %1.3f and now is %1.3f. re-fetching",
lastUpdate.getTime() / 1000f, now / 1000f));
- scheduleDataRetrieval();
+ mainModel.scheduleTransactionListRetrieval();
}
}
- public void scheduleDataRetrieval() {
- Data.scheduleTransactionListRetrieval();
- }
private void createShortcuts(List<MobileLedgerProfile> list) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N_MR1)
return;
else
setTitle(R.string.app_name);
- if (this.profile != null)
- this.profile.getDisplayedAccounts()
- .removeObservers(this);
+ mainModel.setProfile(profile);
this.profile = profile;
mProfileListAdapter.notifyDataSetChanged();
- Data.transactions.clear();
- Logger.debug("transactions", "requesting list reload");
- TransactionListViewModel.scheduleTransactionListReload();
+ mainModel.clearTransactions();
if (haveProfile) {
- profile.scheduleAccountListReload();
+ mainModel.scheduleAccountListReload();
+ Logger.debug("transactions", "requesting list reload");
+ mainModel.scheduleTransactionListReload();
if (profile.isPostingPermitted()) {
mToolbar.setSubtitle(null);
// un-hook all observed LiveData
Data.removeProfileObservers(this);
Data.profiles.removeObservers(this);
- Data.lastUpdateDate.removeObservers(this);
+ mainModel.lastUpdateDate.removeObservers(this);
recreate();
}
}
private void showAccountSummaryFragment() {
mViewPager.setCurrentItem(0, true);
- Data.accountFilter.setValue(null);
+ mainModel.getAccountFilter()
+ .setValue(null);
}
public void onLatestTransactionsClicked(View view) {
drawer.closeDrawers();
showTransactionsFragment(null);
}
public void showTransactionsFragment(String accName) {
- Data.accountFilter.setValue(accName);
+ mainModel.getAccountFilter()
+ .setValue(accName);
mViewPager.setCurrentItem(1, true);
}
public void showAccountTransactions(String accountName) {
}
else {
if (mBackMeansToAccountList && (mViewPager.getCurrentItem() == 1)) {
- Data.accountFilter.setValue(null);
+ mainModel.getAccountFilter()
+ .setValue(null);
showAccountSummaryFragment();
mBackMeansToAccountList = false;
}
long last_update = profile.getLongOption(MLDB.OPT_LAST_SCRAPE, 0L);
- Logger.debug("transactions", String.format(Locale.ENGLISH, "Last update = %d", last_update));
+ Logger.debug("transactions",
+ String.format(Locale.ENGLISH, "Last update = %d", last_update));
if (last_update == 0) {
- Data.lastUpdateDate.postValue(null);
+ mainModel.lastUpdateDate.postValue(null);
}
else {
- Data.lastUpdateDate.postValue(new Date(last_update));
+ mainModel.lastUpdateDate.postValue(new Date(last_update));
}
}
public void onStopTransactionRefreshClick(View view) {
Logger.debug("interactive", "Cancelling transactions refresh");
- Data.stopTransactionsRetrieval();
+ mainModel.stopTransactionsRetrieval();
bTransactionListCancelDownload.setEnabled(false);
}
public void onRetrieveRunningChanged(Boolean running) {
Logger.debug("progress", "Done");
findViewById(R.id.transaction_progress_layout).setVisibility(View.GONE);
- Data.transactionRetrievalDone();
+ mainModel.transactionRetrievalDone();
if (progress.getError() != null) {
Snackbar.make(mViewPager, progress.getError(), Snackbar.LENGTH_LONG)
return;
}
- updateLastUpdateTextFromDB();
-
- new RefreshDescriptionsTask().executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
- TransactionListViewModel.scheduleTransactionListReload();
-
return;
}
if (progressBar.isIndeterminate()) {
progressBar.setIndeterminate(false);
}
- Logger.debug("progress",
- String.format(Locale.US, "%d/%d", progress.getProgress(), progress.getTotal()));
+// Logger.debug("progress",
+// String.format(Locale.US, "%d/%d", progress.getProgress(), progress.getTotal
+// ()));
progressBar.setMax(progress.getTotal());
// for some reason animation doesn't work - no progress is shown (stick at 0)
// on lineageOS 14.1 (Nougat, 7.1.2)
@NotNull
@Override
public Fragment getItem(int position) {
- Logger.debug("main", String.format(Locale.ENGLISH, "Switching to fragment %d", position));
+ Logger.debug("main",
+ String.format(Locale.ENGLISH, "Switching to fragment %d", position));
switch (position) {
case 0:
// debug("flow", "Creating account summary fragment");
profileUUID, transactionId));
tr = profile.loadTransaction(transactionId);
- ArrayList<LedgerTransactionAccount> accounts = tr.getAccounts();
+ List<LedgerTransactionAccount> accounts = tr.getAccounts();
NewTransactionModel.Item firstNegative = null;
NewTransactionModel.Item firstPositive = null;
int singleNegativeIndex = -1;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.Collections;
-import java.util.Locale;
import static net.ktnx.mobileledger.utils.Logger.debug;
throw new AssertionError();
final MobileLedgerProfile profile = profiles.get(position);
final MobileLedgerProfile currentProfile = Data.getProfile();
- debug("profiles", String.format(Locale.ENGLISH, "pos %d: %s, current: %s", position,
- profile.getUuid(), currentProfile.getUuid()));
+// debug("profiles", String.format(Locale.ENGLISH, "pos %d: %s, current: %s", position,
+// profile.getUuid(), currentProfile.getUuid()));
holder.itemView.setTag(profile);
int hue = profile.getThemeHue();
import net.ktnx.mobileledger.App;
import net.ktnx.mobileledger.R;
-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.model.TransactionListItem;
+import net.ktnx.mobileledger.ui.MainModel;
import net.ktnx.mobileledger.utils.Colors;
import net.ktnx.mobileledger.utils.Globals;
+import net.ktnx.mobileledger.utils.Logger;
import net.ktnx.mobileledger.utils.Misc;
import net.ktnx.mobileledger.utils.SimpleDate;
import java.text.DateFormat;
import java.util.GregorianCalendar;
+import java.util.List;
import java.util.Locale;
import java.util.TimeZone;
public class TransactionListAdapter extends RecyclerView.Adapter<TransactionRowHolder> {
- private MobileLedgerProfile profile;
+ private MainModel model;
private AsyncListDiffer<TransactionListItem> listDiffer;
- public TransactionListAdapter() {
+ public TransactionListAdapter(MainModel model) {
super();
+ this.model = model;
+
listDiffer = new AsyncListDiffer<>(this, new DiffUtil.ItemCallback<TransactionListItem>() {
@Override
public boolean areItemsTheSame(@NonNull TransactionListItem oldItem,
});
}
public void onBindViewHolder(@NonNull TransactionRowHolder holder, int position) {
- TransactionListItem item = TransactionListViewModel.getTransactionListItem(position);
+ TransactionListItem item = listDiffer.getCurrentList()
+ .get(position);
// in a race when transaction value is reduced, but the model hasn't been notified yet
// the view will disappear when the notifications reaches the model, so by simply omitting
TransactionLoader loader = new TransactionLoader();
loader.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR,
- new TransactionLoaderParams(tr, holder, position,
- Data.accountFilter.getValue(), item.isOdd()));
+ new TransactionLoaderParams(tr, holder, position, model.getAccountFilter()
+ .getValue()));
// WORKAROUND what seems to be a bug in CardHolder somewhere
// when a view that was previously holding a delimiter is re-purposed
@Override
public int getItemCount() {
- return Data.transactions.size();
+ return listDiffer.getCurrentList()
+ .size();
+ }
+ public void setTransactions(List<TransactionListItem> newList) {
+ Logger.debug("transactions",
+ String.format(Locale.US, "Got new transaction list (%d items)", newList.size()));
+ listDiffer.submitList(newList);
}
enum LoaderStep {HEAD, ACCOUNTS, DONE}
@Override
protected Void doInBackground(TransactionLoaderParams... p) {
LedgerTransaction tr = p[0].transaction;
- boolean odd = p[0].odd;
SQLiteDatabase db = App.getDatabase();
tr.loadData(db);
- publishProgress(new TransactionLoaderStep(p[0].holder, p[0].position, tr, odd));
+ publishProgress(new TransactionLoaderStep(p[0].holder, p[0].position, tr));
int rowIndex = 0;
// FIXME ConcurrentModificationException in ArrayList$ltr.next (ArrayList.java:831)
TransactionRowHolder holder;
int position;
String boldAccountName;
- boolean odd;
TransactionLoaderParams(LedgerTransaction transaction, TransactionRowHolder holder,
- int position, String boldAccountName, boolean odd) {
+ int position, String boldAccountName) {
this.transaction = transaction;
this.holder = holder;
this.position = position;
this.boldAccountName = boldAccountName;
- this.odd = odd;
}
}
}
\ No newline at end of file
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
+import androidx.lifecycle.ViewModelProvider;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
-import com.google.android.material.snackbar.Snackbar;
-
import net.ktnx.mobileledger.R;
import net.ktnx.mobileledger.async.TransactionDateFinder;
import net.ktnx.mobileledger.model.Data;
+import net.ktnx.mobileledger.model.MobileLedgerProfile;
import net.ktnx.mobileledger.ui.DatePickerFragment;
+import net.ktnx.mobileledger.ui.MainModel;
import net.ktnx.mobileledger.ui.MobileLedgerListFragment;
import net.ktnx.mobileledger.ui.activity.MainActivity;
import net.ktnx.mobileledger.utils.Colors;
private MenuItem menuTransactionListFilter;
private View vAccountFilter;
private AutoCompleteTextView accNameFilter;
+ private MainModel model;
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
MainActivity mainActivity = getMainActivity();
+ model = new ViewModelProvider(requireActivity()).get(MainModel.class);
+
refreshLayout = mainActivity.findViewById(R.id.transaction_swipe);
if (refreshLayout == null)
throw new RuntimeException("Can't get hold on the swipe layout");
root = mainActivity.findViewById(R.id.transaction_root);
if (root == null)
throw new RuntimeException("Can't get hold on the transaction value view");
- modelAdapter = new TransactionListAdapter();
+ modelAdapter = new TransactionListAdapter(model);
root.setAdapter(modelAdapter);
mainActivity.fabShouldShow();
refreshLayout.setOnRefreshListener(() -> {
debug("ui", "refreshing transactions via swipe");
- mainActivity.scheduleDataRetrieval();
+ model.scheduleTransactionListRetrieval();
});
Colors.themeWatch.observe(getViewLifecycleOwner(), this::themeChanged);
accNameFilter.setOnItemClickListener((parent, view, position, id) -> {
// debug("tmp", "direct onItemClick");
Cursor c = (Cursor) parent.getItemAtPosition(position);
- Data.accountFilter.setValue(c.getString(1));
+ model.getAccountFilter()
+ .setValue(c.getString(1));
Globals.hideSoftKeyboard(mainActivity);
});
- Data.accountFilter.observe(getViewLifecycleOwner(), this::onAccountNameFilterChanged);
+ model.getAccountFilter()
+ .observe(getViewLifecycleOwner(), this::onAccountNameFilterChanged);
- TransactionListViewModel.updating.addObserver(
- (o, arg) -> refreshLayout.setRefreshing(TransactionListViewModel.updating.get()));
- TransactionListViewModel.updateError.addObserver((o, arg) -> {
- String err = TransactionListViewModel.updateError.get();
- if (err == null)
- return;
-
- Snackbar.make(this.root, err, Snackbar.LENGTH_LONG)
- .show();
- TransactionListViewModel.updateError.set(null);
- });
- Data.transactions.addObserver(
- (o, arg) -> mainActivity.runOnUiThread(() -> modelAdapter.notifyDataSetChanged()));
+ model.getUpdatingFlag()
+ .observe(getViewLifecycleOwner(), (flag) -> refreshLayout.setRefreshing(flag));
+ MobileLedgerProfile profile = Data.getProfile();
+ model.getDisplayedTransactions()
+ .observe(getViewLifecycleOwner(), list -> modelAdapter.setTransactions(list));
mainActivity.findViewById(R.id.clearAccountNameFilter)
.setOnClickListener(v -> {
- Data.accountFilter.setValue(null);
+ model.getAccountFilter()
+ .setValue(null);
vAccountFilter.setVisibility(View.GONE);
menuTransactionListFilter.setVisible(true);
Globals.hideSoftKeyboard(mainActivity);
});
- Data.foundTransactionItemIndex.observe(getViewLifecycleOwner(), pos -> {
+ model.foundTransactionItemIndex.observe(getViewLifecycleOwner(), pos -> {
Logger.debug("go-to-date", String.format(Locale.US, "Found pos %d", pos));
if (pos != null) {
root.scrollToPosition(pos);
// reset the value to avoid re-notification upon reconfiguration or app restart
- Data.foundTransactionItemIndex.setValue(null);
+ model.foundTransactionItemIndex.setValue(null);
}
});
}
if (menuTransactionListFilter != null)
menuTransactionListFilter.setVisible(!filterActive);
- TransactionListViewModel.scheduleTransactionListReload();
+ model.scheduleTransactionListReload();
}
@Override
if ((menuTransactionListFilter == null))
throw new AssertionError();
- if ((Data.accountFilter.getValue() != null) ||
- (vAccountFilter.getVisibility() == View.VISIBLE))
+ if ((model.getAccountFilter()
+ .getValue() != null) || (vAccountFilter.getVisibility() == View.VISIBLE))
{
menuTransactionListFilter.setVisible(false);
}
.setOnMenuItemClickListener(item -> {
DatePickerFragment picker = new DatePickerFragment();
picker.setOnDatePickedListener(this);
- picker.setDateRange(Data.earliestTransactionDate.getValue(),
- Data.latestTransactionDate.getValue());
+ picker.setDateRange(model.getFirstTransactionDate(),
+ model.getLastTransactionDate());
picker.show(requireActivity().getSupportFragmentManager(), null);
return true;
});
@Override
public void onDatePicked(int year, int month, int day) {
RecyclerView list = requireActivity().findViewById(R.id.transaction_root);
- AsyncTask<SimpleDate, Void, Integer> finder = new TransactionDateFinder();
+ AsyncTask<TransactionDateFinder.Params, Void, Integer> finder = new TransactionDateFinder();
- finder.execute(new SimpleDate(year, month + 1, day));
+ finder.execute(
+ new TransactionDateFinder.Params(model, new SimpleDate(year, month + 1, day)));
}
}
+++ /dev/null
-/*
- * Copyright © 2019 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
- * the Free Software Foundation, either version 3 of the License, or
- * (at your opinion), any later version.
- *
- * MoLe is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License terms for details.
- *
- * You should have received a copy of the GNU General Public License
- * along with MoLe. If not, see <https://www.gnu.org/licenses/>.
- */
-
-package net.ktnx.mobileledger.ui.transaction_list;
-
-import android.os.AsyncTask;
-
-import androidx.lifecycle.ViewModel;
-
-import net.ktnx.mobileledger.async.UpdateTransactionsTask;
-import net.ktnx.mobileledger.model.Data;
-import net.ktnx.mobileledger.model.TransactionListItem;
-import net.ktnx.mobileledger.utils.LockHolder;
-import net.ktnx.mobileledger.utils.ObservableValue;
-
-public class TransactionListViewModel extends ViewModel {
- public static ObservableValue<Boolean> updating = new ObservableValue<>();
- public static ObservableValue<String> updateError = new ObservableValue<>();
-
- public static void scheduleTransactionListReload() {
- String filter = Data.accountFilter.getValue();
- AsyncTask<String, Void, String> task = new UTT();
- task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, filter);
- }
- public static TransactionListItem getTransactionListItem(int position) {
- try(LockHolder lh = Data.transactions.lockForReading()) {
- if (position >= Data.transactions.size()) return null;
- return Data.transactions.get(position);
- }
- }
- private static class UTT extends UpdateTransactionsTask {
- @Override
- protected void onPostExecute(String error) {
- super.onPostExecute(error);
- if (error != null) updateError.set(error);
- }
- }
-}
/*
- * Copyright © 2019 Damyan Ivanov.
+ * Copyright © 2020 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
private LedgerTransactionAccount account;
private int accountPosition;
private String boldAccountName;
- private boolean odd;
public TransactionLoaderStep(TransactionRowHolder holder, int position,
- LedgerTransaction transaction, boolean isOdd) {
+ LedgerTransaction transaction) {
this.step = TransactionListAdapter.LoaderStep.HEAD;
this.holder = holder;
this.transaction = transaction;
this.position = position;
- this.odd = isOdd;
}
public TransactionLoaderStep(TransactionRowHolder holder, LedgerTransactionAccount account,
int accountPosition, String boldAccountName) {
public LedgerTransactionAccount getAccount() {
return account;
}
- public boolean isOdd() {
- return odd;
- }
}
public class MobileLedgerDatabase extends SQLiteOpenHelper {
private static final String DB_NAME = "MoLe.db";
- private static final int LATEST_REVISION = 37;
+ private static final int LATEST_REVISION = 39;
private static final String CREATE_DB_SQL = "create_db";
private final Application mContext;
public void onOpen(SQLiteDatabase db) {
super.onOpen(db);
db.execSQL("pragma case_sensitive_like=ON;");
-// db.execSQL("PRAGMA foreign_keys=ON");
+ db.execSQL("PRAGMA foreign_keys=ON");
}
private void applyRevision(SQLiteDatabase db, int rev_no) {
return Integer.compare(day, date.day);
}
+ public Calendar asCalendar() {
+ final Calendar calendar = Calendar.getInstance();
+ calendar.set(year, month, day);
+ return calendar;
+ }
}
create unique index un_options on options(profile,name);
create table account_values(profile varchar not null, account varchar not null, currency varchar not null default '', value decimal not null, generation integer default 0 );
create unique index un_account_values on account_values(profile,account,currency);
-create table description_history(description varchar not null primary key, description_upper varchar);
+create table description_history(description varchar not null primary key, description_upper varchar, generation integer default 0);
+create unique index un_description_history on description_history(description_upper);
create table profiles(uuid varchar not null primary key, name not null, url not null, use_authentication boolean not null, auth_user varchar, auth_password varchar, order_no integer, permit_posting boolean default 0, theme integer default -1, preferred_accounts_filter varchar, future_dates integer, api_version integer, show_commodity_by_default boolean default 0, default_commodity varchar, show_comments_by_default boolean default 1);
create table transactions(profile varchar not null, id integer not null, data_hash varchar not null, year integer not null, month integer not null, day integer not null, description varchar not null, comment varchar, generation integer default 0);
create unique index un_transactions_id on transactions(profile,id);
create unique index un_transactions_data_hash on transactions(profile,data_hash);
create index idx_transaction_description on transactions(description);
-create table transaction_accounts(profile varchar not null, transaction_id integer not null, order_no integer not null, account_name varchar not null, currency varchar not null default '', amount decimal not null, comment varchar, constraint fk_transaction_accounts_acc foreign key(profile,account_name) references accounts(profile,account_name), constraint fk_transaction_accounts_trn foreign key(profile, transaction_id) references transactions(profile,id), generation integer default 0);
+create table transaction_accounts(profile varchar not null, transaction_id integer not null, order_no integer not null, account_name varchar not null, currency varchar not null default '', amount decimal not null, comment varchar, constraint fk_transaction_accounts_acc foreign key(profile,account_name) references accounts(profile,name), constraint fk_transaction_accounts_trn foreign key(profile, transaction_id) references transactions(profile,id), generation integer default 0);
create unique index un_transaction_accounts_order on transaction_accounts(profile, transaction_id, order_no);
create table currencies(id integer not null primary key, name varchar not null, position varchar not null, has_gap boolean not null);
--- updated to revision 37
\ No newline at end of file
+-- updated to revision 39
\ No newline at end of file
--- /dev/null
+-- Copyright © 2020 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
+-- the Free Software Foundation, either version 3 of the License, or
+-- (at your opinion), any later version.
+--
+-- MoLe is distributed in the hope that it will be useful,
+-- but WITHOUT ANY WARRANTY; without even the implied warranty of
+-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+-- GNU General Public License terms for details.
+--
+-- You should have received a copy of the GNU General Public License
+-- along with MoLe. If not, see <https://www.gnu.org/licenses/>.
+CREATE TABLE transaction_accounts_new(profile varchar not null, transaction_id integer not null, account_name varchar not null, currency varchar not null default '', amount decimal not null, comment varchar, generation integer default 0, order_no integer not null default 0, constraint fk_transaction_accounts_acc foreign key(profile,account_name) references accounts(profile,name), constraint fk_transaction_accounts_trn foreign key(profile, transaction_id) references transactions(profile,id));
+insert into transaction_accounts_new(profile, transaction_id, account_name, currency, amount, comment, generation, order_no) select profile, transaction_id, account_name, currency, amount, comment, generation, order_no from transaction_accounts;
+drop table transaction_accounts;
+alter table transaction_accounts_new rename to transaction_accounts;
+create unique index un_transaction_accounts_order on transaction_accounts(profile, transaction_id, order_no);
\ No newline at end of file
--- /dev/null
+-- Copyright © 2020 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
+-- the Free Software Foundation, either version 3 of the License, or
+-- (at your opinion), any later version.
+--
+-- MoLe is distributed in the hope that it will be useful,
+-- but WITHOUT ANY WARRANTY; without even the implied warranty of
+-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+-- GNU General Public License terms for details.
+--
+-- You should have received a copy of the GNU General Public License
+-- along with MoLe. If not, see <https://www.gnu.org/licenses/>.
+create table description_history_new(description varchar not null primary key, description_upper varchar, generation integer default 0);
+insert into description_history_new(description, description_upper) select description, description_upper from description_history;
+drop table description_history;
+alter table description_history_new rename to description_history;
+create unique index un_description_history on description_history(description_upper);
\ No newline at end of file