]> git.ktnx.net Git - mobile-ledger.git/commitdiff
major rework of parsed transaction/descriptions/accounts storage
authorDamyan Ivanov <dam+mobileledger@ktnx.net>
Wed, 16 Sep 2020 13:30:50 +0000 (16:30 +0300)
committerDamyan Ivanov <dam+mobileledger@ktnx.net>
Wed, 16 Sep 2020 13:30:50 +0000 (16:30 +0300)
new approach avoids mass deletion followed by mass inserts and allows
activation of foreign key constraints in SQLite

the list of accounts and transactions are managed by an
asynchronous differ

members of Data that are relevant only to the MainActivity and its
fragments are moved to a new MainModel ViewModel class

25 files changed:
app/src/main/java/net/ktnx/mobileledger/async/RefreshDescriptionsTask.java [deleted file]
app/src/main/java/net/ktnx/mobileledger/async/RetrieveTransactionsTask.java
app/src/main/java/net/ktnx/mobileledger/async/TransactionAccumulator.java [new file with mode: 0644]
app/src/main/java/net/ktnx/mobileledger/async/TransactionDateFinder.java
app/src/main/java/net/ktnx/mobileledger/async/UpdateTransactionsTask.java
app/src/main/java/net/ktnx/mobileledger/json/v1_15/ParsedLedgerTransaction.java
app/src/main/java/net/ktnx/mobileledger/model/Data.java
app/src/main/java/net/ktnx/mobileledger/model/LedgerTransaction.java
app/src/main/java/net/ktnx/mobileledger/model/MobileLedgerProfile.java
app/src/main/java/net/ktnx/mobileledger/model/TransactionListItem.java
app/src/main/java/net/ktnx/mobileledger/ui/MainModel.java [new file with mode: 0644]
app/src/main/java/net/ktnx/mobileledger/ui/account_summary/AccountSummaryAdapter.java
app/src/main/java/net/ktnx/mobileledger/ui/account_summary/AccountSummaryFragment.java
app/src/main/java/net/ktnx/mobileledger/ui/activity/MainActivity.java
app/src/main/java/net/ktnx/mobileledger/ui/activity/NewTransactionItemsAdapter.java
app/src/main/java/net/ktnx/mobileledger/ui/profiles/ProfilesRecyclerViewAdapter.java
app/src/main/java/net/ktnx/mobileledger/ui/transaction_list/TransactionListAdapter.java
app/src/main/java/net/ktnx/mobileledger/ui/transaction_list/TransactionListFragment.java
app/src/main/java/net/ktnx/mobileledger/ui/transaction_list/TransactionListViewModel.java [deleted file]
app/src/main/java/net/ktnx/mobileledger/ui/transaction_list/TransactionLoaderStep.java
app/src/main/java/net/ktnx/mobileledger/utils/MobileLedgerDatabase.java
app/src/main/java/net/ktnx/mobileledger/utils/SimpleDate.java
app/src/main/res/raw/create_db.sql
app/src/main/res/raw/sql_38.sql [new file with mode: 0644]
app/src/main/res/raw/sql_39.sql [new file with mode: 0644]

diff --git a/app/src/main/java/net/ktnx/mobileledger/async/RefreshDescriptionsTask.java b/app/src/main/java/net/ktnx/mobileledger/async/RefreshDescriptionsTask.java
deleted file mode 100644 (file)
index 50e7047..0000000
+++ /dev/null
@@ -1,73 +0,0 @@
-/*
- * 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;
-    }
-}
index ac9ec5093e54ece20790e6107f8fe2d593c5d9af..71131a075b9f3c2b183fcca8989548e731393fdd 100644 (file)
@@ -36,6 +36,7 @@ import net.ktnx.mobileledger.model.LedgerAccount;
 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;
@@ -48,15 +49,17 @@ import java.net.URLDecoder;
 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(
@@ -74,10 +77,16 @@ public class RetrieveTransactionsTask
     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);
@@ -115,11 +124,11 @@ public class RetrieveTransactionsTask
         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
@@ -129,15 +138,14 @@ public class RetrieveTransactionsTask
         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<>();
 
@@ -202,7 +210,7 @@ public class RetrieveTransactionsTask
                             }
                             lastAccount = new LedgerAccount(profile, accName, parentAccount);
 
-                            list.add(lastAccount);
+                            accounts.add(lastAccount);
                             map.put(accName, lastAccount);
 
                             state = ParserState.EXPECTING_ACCOUNT_AMOUNT;
@@ -284,7 +292,7 @@ public class RetrieveTransactionsTask
                         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 {
@@ -295,8 +303,8 @@ public class RetrieveTransactionsTask
                                         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,
@@ -345,10 +353,6 @@ public class RetrieveTransactionsTask
             }
 
             throwIfCancelled();
-
-            profile.setAndStoreAccountAndTransactionListFromWeb(list, transactions);
-
-            return null;
         }
     }
     private @NonNull
@@ -372,14 +376,14 @@ public class RetrieveTransactionsTask
         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());
         }
@@ -388,9 +392,11 @@ public class RetrieveTransactionsTask
         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()));
 
@@ -459,12 +465,11 @@ public class RetrieveTransactionsTask
             }
         }
 
-        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");
@@ -474,13 +479,13 @@ public class RetrieveTransactionsTask
             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);
 
@@ -498,44 +503,73 @@ public class RetrieveTransactionsTask
 
                 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();
@@ -624,4 +658,17 @@ public class RetrieveTransactionsTask
             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;
+        }
+    }
 }
diff --git a/app/src/main/java/net/ktnx/mobileledger/async/TransactionAccumulator.java b/app/src/main/java/net/ktnx/mobileledger/async/TransactionAccumulator.java
new file mode 100644 (file)
index 0000000..8f57e76
--- /dev/null
@@ -0,0 +1,58 @@
+/*
+ * 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);
+    }
+}
index c15e59906e9932d4ca9453d0aecf5aefd3f1b97b..e90a2ae32381032b6dcff00386ed3dce4e8a621f 100644 (file)
@@ -19,9 +19,8 @@ package net.ktnx.mobileledger.async;
 
 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;
 
@@ -29,32 +28,45 @@ import java.util.Collections;
 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) {
index 404e5127b4fdaa061399802dcc097d66d8a04e71..43de74025ad83ca7dc8dd511080dcc60d2d3c55c 100644 (file)
@@ -25,26 +25,24 @@ import net.ktnx.mobileledger.App;
 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};
@@ -57,45 +55,26 @@ public class UpdateTransactionsTask extends AsyncTask<String, Void, String> {
                       "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 {
index 8b1fab78d4eea93c744bf87bf01600856d622666..9a9bfdc0bf4fd79760e4e2155a698a2420c3431f 100644 (file)
@@ -153,6 +153,8 @@ public class ParsedLedgerTransaction implements net.ktnx.mobileledger.json.Parse
                 tr.addAccount(p.asLedgerAccount());
             }
         }
+
+        tr.markDataAsLoaded();
         return tr;
     }
 }
index c538f57a2deb266ebf1f323c2d7993efd75d6d44..74e274d672d82e78d3d48b6b9975702294bc3283 100644 (file)
@@ -19,7 +19,6 @@ package net.ktnx.mobileledger.model;
 
 import android.database.Cursor;
 import android.database.sqlite.SQLiteDatabase;
-import android.os.AsyncTask;
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
@@ -33,12 +32,9 @@ 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.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;
@@ -47,20 +43,12 @@ import java.util.concurrent.atomic.AtomicInteger;
 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);
@@ -70,8 +58,6 @@ public final class Data {
             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());
@@ -97,7 +83,6 @@ public final class Data {
     }
     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) {
@@ -165,29 +150,6 @@ public final class Data {
         }
         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();
index 6a8234f28dc8d7145f8cd578db2353f8f5f41aad..d42e5630d0274f26e3525c510a6435d40f1d023e 100644 (file)
@@ -33,6 +33,7 @@ import java.security.NoSuchAlgorithmException;
 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";
@@ -56,7 +57,7 @@ public class LedgerTransaction {
     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)
@@ -91,7 +92,7 @@ public class LedgerTransaction {
         this.dataHash = null;
         this.dataLoaded = false;
     }
-    public ArrayList<LedgerTransactionAccount> getAccounts() {
+    public List<LedgerTransactionAccount> getAccounts() {
         return accounts;
     }
     public void addAccount(LedgerTransactionAccount item) {
@@ -104,7 +105,6 @@ public class LedgerTransaction {
     }
     @NonNull
     public SimpleDate getDate() {
-        loadData(App.getDatabase());
         if (date == null)
             throw new IllegalStateException("Transaction has no date");
         return date;
@@ -130,6 +130,7 @@ public class LedgerTransaction {
         return id;
     }
     protected void fillDataHash() {
+        loadData(App.getDatabase());
         if (dataHash != null)
             return;
         try {
@@ -163,18 +164,7 @@ public class LedgerTransaction {
                     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;
 
@@ -238,4 +228,7 @@ public class LedgerTransaction {
 
         return false;
     }
+    public void markDataAsLoaded() {
+        dataLoaded = true;
+    }
 }
index d9fd260a4533e173aa2ac187a985f941a46d344a..e255176beb008b55417f9145df185a0bf06ffe5d 100644 (file)
@@ -20,32 +20,22 @@ package net.ktnx.mobileledger.model;
 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;
@@ -54,13 +44,8 @@ import java.util.Objects;
 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;
@@ -74,24 +59,13 @@ public final class MobileLedgerProfile {
     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;
@@ -109,13 +83,6 @@ public final class MobileLedgerProfile {
         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;
     }
@@ -174,67 +141,6 @@ public final class MobileLedgerProfile {
             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) {
@@ -272,29 +178,8 @@ public final class MobileLedgerProfile {
             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;
     }
@@ -453,6 +338,7 @@ public final class MobileLedgerProfile {
     }
     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=?",
@@ -614,12 +500,6 @@ public final class MobileLedgerProfile {
                 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();
@@ -673,116 +553,15 @@ public final class MobileLedgerProfile {
             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),
@@ -829,190 +608,34 @@ public final class MobileLedgerProfile {
         }
     }
 
-    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() {
@@ -1024,32 +647,54 @@ public final class MobileLedgerProfile {
                     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();
             }
@@ -1057,5 +702,16 @@ public final class MobileLedgerProfile {
                 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
+                    });
+        }
     }
 }
index 084eaad8e8151d00af628dffcff82234fb684a2e..0aaf763ab564e3e39a9fa0b17e8cb085c7d8058c 100644 (file)
@@ -19,6 +19,7 @@ package net.ktnx.mobileledger.model;
 
 import androidx.annotation.NonNull;
 
+import net.ktnx.mobileledger.App;
 import net.ktnx.mobileledger.utils.SimpleDate;
 
 public class TransactionListItem {
@@ -26,16 +27,14 @@ 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() {
@@ -43,7 +42,10 @@ public class TransactionListItem {
     }
     @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;
@@ -51,8 +53,5 @@ public class TransactionListItem {
     public LedgerTransaction getTransaction() {
         return transaction;
     }
-    public boolean isOdd() {
-        return odd;
-    }
     public enum Type {TRANSACTION, DELIMITER}
 }
diff --git a/app/src/main/java/net/ktnx/mobileledger/ui/MainModel.java b/app/src/main/java/net/ktnx/mobileledger/ui/MainModel.java
new file mode 100644 (file)
index 0000000..9572ea0
--- /dev/null
@@ -0,0 +1,410 @@
+/*
+ * 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");
+        }
+    }
+}
index 2d7ea6ed6e3a8404749a781077b49d1ae4fd19de..d50601efed707a4a21f76ae64be3459604a5a919 100644 (file)
@@ -37,6 +37,7 @@ import net.ktnx.mobileledger.R;
 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;
@@ -52,7 +53,10 @@ public class AccountSummaryAdapter
         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,
@@ -88,7 +92,7 @@ public class AccountSummaryAdapter
     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;
@@ -128,8 +132,8 @@ public class AccountSummaryAdapter
             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;
 
@@ -138,7 +142,7 @@ public class AccountSummaryAdapter
             }
             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()
index 63c7455df543f5ed45c9aae7d22e50da5d56ef3f..29003b820f484ea75c06bb24a15a578bc83eddd7 100644 (file)
@@ -25,6 +25,7 @@ import android.view.ViewGroup;
 
 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;
@@ -32,7 +33,7 @@ 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;
@@ -70,10 +71,12 @@ public class AccountSummaryFragment extends MobileLedgerListFragment {
         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);
@@ -93,12 +96,11 @@ public class AccountSummaryFragment extends MobileLedgerListFragment {
         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",
index 88ab8090cfc4d86bc805c2270076d11d44362dff..5efc2f84f386180a930fd24ea7164d8344efd180 100644 (file)
@@ -25,7 +25,6 @@ import android.content.pm.ShortcutManager;
 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;
@@ -43,6 +42,7 @@ import androidx.drawerlayout.widget.DrawerLayout;
 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;
@@ -51,15 +51,14 @@ import com.google.android.material.floatingactionbutton.FloatingActionButton;
 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;
@@ -95,6 +94,7 @@ public class MainActivity extends ProfileThemedActivity {
     private ActionBarDrawerToggle barDrawerToggle;
     private ViewPager.SimpleOnPageChangeListener pageChangeListener;
     private MobileLedgerProfile profile;
+    private MainModel mainModel;
     @Override
     protected void onStart() {
         super.onStart();
@@ -107,8 +107,10 @@ public class MainActivity extends ProfileThemedActivity {
     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() {
@@ -149,6 +151,8 @@ public class MainActivity extends ProfileThemedActivity {
         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);
@@ -226,10 +230,11 @@ public class MainActivity extends ProfileThemedActivity {
             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));
@@ -319,6 +324,16 @@ public class MainActivity extends ProfileThemedActivity {
             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();
@@ -330,12 +345,9 @@ public class MainActivity extends ProfileThemedActivity {
                         "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;
@@ -403,9 +415,7 @@ public class MainActivity extends ProfileThemedActivity {
         else
             setTitle(R.string.app_name);
 
-        if (this.profile != null)
-            this.profile.getDisplayedAccounts()
-                        .removeObservers(this);
+        mainModel.setProfile(profile);
 
         this.profile = profile;
 
@@ -426,12 +436,12 @@ public class MainActivity extends ProfileThemedActivity {
 
         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);
@@ -472,7 +482,7 @@ public class MainActivity extends ProfileThemedActivity {
         // un-hook all observed LiveData
         Data.removeProfileObservers(this);
         Data.profiles.removeObservers(this);
-        Data.lastUpdateDate.removeObservers(this);
+        mainModel.lastUpdateDate.removeObservers(this);
 
         recreate();
     }
@@ -518,7 +528,8 @@ public class MainActivity extends ProfileThemedActivity {
     }
     private void showAccountSummaryFragment() {
         mViewPager.setCurrentItem(0, true);
-        Data.accountFilter.setValue(null);
+        mainModel.getAccountFilter()
+                 .setValue(null);
     }
     public void onLatestTransactionsClicked(View view) {
         drawer.closeDrawers();
@@ -526,7 +537,8 @@ public class MainActivity extends ProfileThemedActivity {
         showTransactionsFragment(null);
     }
     public void showTransactionsFragment(String accName) {
-        Data.accountFilter.setValue(accName);
+        mainModel.getAccountFilter()
+                 .setValue(accName);
         mViewPager.setCurrentItem(1, true);
     }
     public void showAccountTransactions(String accountName) {
@@ -541,7 +553,8 @@ public class MainActivity extends ProfileThemedActivity {
         }
         else {
             if (mBackMeansToAccountList && (mViewPager.getCurrentItem() == 1)) {
-                Data.accountFilter.setValue(null);
+                mainModel.getAccountFilter()
+                         .setValue(null);
                 showAccountSummaryFragment();
                 mBackMeansToAccountList = false;
             }
@@ -559,17 +572,18 @@ public class MainActivity extends ProfileThemedActivity {
 
         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) {
@@ -600,7 +614,7 @@ public class MainActivity extends ProfileThemedActivity {
             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)
@@ -608,11 +622,6 @@ public class MainActivity extends ProfileThemedActivity {
                 return;
             }
 
-            updateLastUpdateTextFromDB();
-
-            new RefreshDescriptionsTask().executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
-            TransactionListViewModel.scheduleTransactionListReload();
-
             return;
         }
 
@@ -635,8 +644,9 @@ public class MainActivity extends ProfileThemedActivity {
             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)
@@ -665,7 +675,8 @@ public class MainActivity extends ProfileThemedActivity {
         @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");
index 4b3fa8e67a94675e5c0c51205ec34da245d3cb47..5118d2c64db6e8fc35de4c471eb2cd47f5ef8b94 100644 (file)
@@ -283,7 +283,7 @@ class NewTransactionItemsAdapter extends RecyclerView.Adapter<NewTransactionItem
                     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;
index 2716623d6e91e7d2ab867fcf2cae77940362df9d..34b9064a8f4a34023f4810c24dde2fc5a48519d4 100644 (file)
@@ -45,7 +45,6 @@ import net.ktnx.mobileledger.utils.Colors;
 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;
 
@@ -194,8 +193,8 @@ public class ProfilesRecyclerViewAdapter
             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();
index 58468e88c33fef7ddf7ef06ca4fda8bf7dd5be6c..6f13930ad286bc63593f0bf3c7aaac88736fc3c1 100644 (file)
@@ -39,26 +39,29 @@ import androidx.recyclerview.widget.RecyclerView;
 
 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,
@@ -99,7 +102,8 @@ public class TransactionListAdapter extends RecyclerView.Adapter<TransactionRowH
         });
     }
     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
@@ -120,8 +124,8 @@ public class TransactionListAdapter extends RecyclerView.Adapter<TransactionRowH
 
                 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
@@ -169,7 +173,13 @@ public class TransactionListAdapter extends RecyclerView.Adapter<TransactionRowH
 
     @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}
 
@@ -178,12 +188,11 @@ public class TransactionListAdapter extends RecyclerView.Adapter<TransactionRowH
         @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)
@@ -292,14 +301,12 @@ public class TransactionListAdapter extends RecyclerView.Adapter<TransactionRowH
         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
index e936f2f4ec4dcdc7153224b625481ad9faf35024..9564cc8597317c79fa5bd378470c67eaf7ba9e0d 100644 (file)
@@ -31,15 +31,16 @@ import android.widget.AutoCompleteTextView;
 
 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;
@@ -62,6 +63,7 @@ public class TransactionListFragment extends MobileLedgerListFragment
     private MenuItem menuTransactionListFilter;
     private View vAccountFilter;
     private AutoCompleteTextView accNameFilter;
+    private MainModel model;
     @Override
     public void onCreate(@Nullable Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
@@ -98,13 +100,15 @@ public class TransactionListFragment extends MobileLedgerListFragment
 
         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();
@@ -118,7 +122,7 @@ public class TransactionListFragment extends MobileLedgerListFragment
 
         refreshLayout.setOnRefreshListener(() -> {
             debug("ui", "refreshing transactions via swipe");
-            mainActivity.scheduleDataRetrieval();
+            model.scheduleTransactionListRetrieval();
         });
 
         Colors.themeWatch.observe(getViewLifecycleOwner(), this::themeChanged);
@@ -130,40 +134,35 @@ public class TransactionListFragment extends MobileLedgerListFragment
         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);
             }
         });
     }
@@ -183,7 +182,7 @@ public class TransactionListFragment extends MobileLedgerListFragment
         if (menuTransactionListFilter != null)
             menuTransactionListFilter.setVisible(!filterActive);
 
-        TransactionListViewModel.scheduleTransactionListReload();
+        model.scheduleTransactionListReload();
 
     }
     @Override
@@ -194,8 +193,8 @@ public class TransactionListFragment extends MobileLedgerListFragment
         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);
         }
@@ -218,8 +217,8 @@ public class TransactionListFragment extends MobileLedgerListFragment
             .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;
             });
@@ -227,8 +226,9 @@ public class TransactionListFragment extends MobileLedgerListFragment
     @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)));
     }
 }
diff --git a/app/src/main/java/net/ktnx/mobileledger/ui/transaction_list/TransactionListViewModel.java b/app/src/main/java/net/ktnx/mobileledger/ui/transaction_list/TransactionListViewModel.java
deleted file mode 100644 (file)
index f994ec7..0000000
+++ /dev/null
@@ -1,52 +0,0 @@
-/*
- * 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);
-        }
-    }
-}
index 1dd860473c7967907e86e3a618b62505ab8528a4..c6c077840d1df549a3062aac01ffcfb9f0d163b2 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * 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
@@ -29,14 +29,12 @@ class TransactionLoaderStep {
     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) {
@@ -76,7 +74,4 @@ class TransactionLoaderStep {
     public LedgerTransactionAccount getAccount() {
         return account;
     }
-    public boolean isOdd() {
-        return odd;
-    }
 }
index fc03a9f8cfc5431ee7ebfd2fc0b841bc0a78b74b..3e1919e17a6516cd09d6c7892439f48a04ed02f6 100644 (file)
@@ -34,7 +34,7 @@ import static net.ktnx.mobileledger.utils.Logger.debug;
 
 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;
@@ -62,7 +62,7 @@ public class MobileLedgerDatabase extends SQLiteOpenHelper {
     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) {
index 28ad751d0557531b94513cbe22bfe2d186c048c7..c8c3e054c2fd54606bb832bd8b52d842babce0b8 100644 (file)
@@ -91,4 +91,9 @@ public class SimpleDate implements Comparable<SimpleDate> {
 
         return Integer.compare(day, date.day);
     }
+    public Calendar asCalendar() {
+        final Calendar calendar = Calendar.getInstance();
+        calendar.set(year, month, day);
+        return calendar;
+    }
 }
index 69be40d3d1cb0988283306843aafbd646394409e..dc96f4bb628bb85e336b5b61198e2e45d82f6f06 100644 (file)
@@ -18,13 +18,14 @@ create table options(profile varchar not null, name varchar not null, value varc
 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
diff --git a/app/src/main/res/raw/sql_38.sql b/app/src/main/res/raw/sql_38.sql
new file mode 100644 (file)
index 0000000..60c8611
--- /dev/null
@@ -0,0 +1,19 @@
+-- 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
diff --git a/app/src/main/res/raw/sql_39.sql b/app/src/main/res/raw/sql_39.sql
new file mode 100644 (file)
index 0000000..0312574
--- /dev/null
@@ -0,0 +1,19 @@
+-- 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