another major rework, transaction list is fully asynchronous
authorDamyan Ivanov <dam+mobileledger@ktnx.net>
Fri, 4 Jan 2019 17:56:37 +0000 (17:56 +0000)
committerDamyan Ivanov <dam+mobileledger@ktnx.net>
Fri, 4 Jan 2019 17:56:37 +0000 (17:56 +0000)
16 files changed:
app/src/main/java/net/ktnx/mobileledger/async/RetrieveAccountsTask.java
app/src/main/java/net/ktnx/mobileledger/async/RetrieveTransactionsTask.java
app/src/main/java/net/ktnx/mobileledger/async/UpdateTransactionsTask.java [new file with mode: 0644]
app/src/main/java/net/ktnx/mobileledger/model/Data.java [new file with mode: 0644]
app/src/main/java/net/ktnx/mobileledger/model/LedgerTransaction.java
app/src/main/java/net/ktnx/mobileledger/model/ObservableAtomicInteger.java [new file with mode: 0644]
app/src/main/java/net/ktnx/mobileledger/model/ObservableValue.java [new file with mode: 0644]
app/src/main/java/net/ktnx/mobileledger/model/TransactionList.java [new file with mode: 0644]
app/src/main/java/net/ktnx/mobileledger/ui/MobileLedgerListFragment.java [new file with mode: 0644]
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/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
app/src/main/java/net/ktnx/mobileledger/ui/transaction_list/TransactionLoaderStep.java [new file with mode: 0644]
app/src/main/java/net/ktnx/mobileledger/ui/transaction_list/TransactionRowHolder.java [new file with mode: 0644]

index 94eaa1bdd59a849f4585c8f72d1572df48211465..a27b1c8cb5b27b5c2b0567726c1a139026cbaff8 100644 (file)
@@ -24,7 +24,7 @@ import android.util.Log;
 
 import net.ktnx.mobileledger.R;
 import net.ktnx.mobileledger.model.LedgerAccount;
-import net.ktnx.mobileledger.ui.account_summary.AccountSummaryFragment;
+import net.ktnx.mobileledger.ui.activity.MainActivity;
 import net.ktnx.mobileledger.utils.MLDB;
 import net.ktnx.mobileledger.utils.NetworkUtil;
 
@@ -42,10 +42,10 @@ import java.util.regex.Pattern;
 
 public class RetrieveAccountsTask extends android.os.AsyncTask<Void, Integer, Void> {
     int error;
-    WeakReference<AccountSummaryFragment> mContext;
+    WeakReference<MainActivity> mContext;
     private SharedPreferences pref;
 
-    public RetrieveAccountsTask(WeakReference<AccountSummaryFragment> context) {
+    public RetrieveAccountsTask(WeakReference<MainActivity> context) {
         mContext = context;
         error = 0;
     }
@@ -82,9 +82,9 @@ public class RetrieveAccountsTask extends android.os.AsyncTask<Void, Integer, Vo
                             // %3A is '='
                             Pattern account_name_re =
                                     Pattern.compile("/register\\?q=inacct%3A([a-zA-Z0-9%]+)\"");
-                            Pattern value_re = Pattern.compile(
+                            Pattern account_value_re = Pattern.compile(
                                     "<span class=\"[^\"]*\\bamount\\b[^\"]*\">\\s*([-+]?[\\d.,]+)(?:\\s+(\\S+))?</span>");
-                            Pattern tr_re = Pattern.compile("</tr>");
+                            Pattern tr_end_re = Pattern.compile("</tr>");
                             Pattern descriptions_line_re =
                                     Pattern.compile("\\bdescriptionsSuggester\\s*=\\s*new\\b");
                             Pattern description_items_re =
@@ -108,7 +108,7 @@ public class RetrieveAccountsTask extends android.os.AsyncTask<Void, Integer, Vo
                                     continue;
                                 }
 
-                                Matcher tr_m = tr_re.matcher(line);
+                                Matcher tr_m = tr_end_re.matcher(line);
                                 if (tr_m.find()) {
                                     Log.d("account-parser", "<tr> - another account expected");
                                     last_account_name = null;
@@ -116,7 +116,7 @@ public class RetrieveAccountsTask extends android.os.AsyncTask<Void, Integer, Vo
                                 }
 
                                 if (last_account_name != null) {
-                                    m = value_re.matcher(line);
+                                    m = account_value_re.matcher(line);
                                     boolean match_found = false;
                                     while (m.find()) {
                                         throwIfCancelled();
@@ -208,9 +208,9 @@ public class RetrieveAccountsTask extends android.os.AsyncTask<Void, Integer, Vo
     }
     @Override
     protected void onPostExecute(Void result) {
-        AccountSummaryFragment ctx = mContext.get();
+        MainActivity ctx = mContext.get();
         if (ctx == null) return;
-        ctx.onAccountRefreshDone(this.error);
+        ctx.onRetrieveDone(this.error == 0);
     }
 
 }
index 57643f5e711fba01eaba29aa943cf5eec2eb947b..7f4246cf6e892b57cc2c29bc48a44b10013e4686 100644 (file)
@@ -21,12 +21,16 @@ import android.annotation.SuppressLint;
 import android.content.SharedPreferences;
 import android.database.sqlite.SQLiteDatabase;
 import android.os.AsyncTask;
+import android.os.OperationCanceledException;
 import android.util.Log;
 
 import net.ktnx.mobileledger.R;
+import net.ktnx.mobileledger.model.Data;
+import net.ktnx.mobileledger.model.LedgerAccount;
 import net.ktnx.mobileledger.model.LedgerTransaction;
 import net.ktnx.mobileledger.model.LedgerTransactionAccount;
-import net.ktnx.mobileledger.ui.transaction_list.TransactionListFragment;
+import net.ktnx.mobileledger.ui.activity.MainActivity;
+import net.ktnx.mobileledger.ui.transaction_list.TransactionListViewModel;
 import net.ktnx.mobileledger.utils.MLDB;
 import net.ktnx.mobileledger.utils.NetworkUtil;
 
@@ -38,7 +42,10 @@ import java.io.InputStreamReader;
 import java.lang.ref.WeakReference;
 import java.net.HttpURLConnection;
 import java.net.MalformedURLException;
+import java.net.URLDecoder;
+import java.util.ArrayList;
 import java.util.Date;
+import java.util.List;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 
@@ -52,40 +59,47 @@ public class RetrieveTransactionsTask extends
     private static final Pattern transactionDetailsPattern =
             Pattern.compile("^\\s+" + "(\\S[\\S\\s]+\\S)\\s\\s+([-+]?\\d[\\d,.]*)(?:\\s+(\\S+)$)?");
     private static final Pattern endPattern = Pattern.compile("\\bid=\"addmodal\"");
-    protected WeakReference<TransactionListFragment> contextRef;
+    protected WeakReference<MainActivity> contextRef;
     protected int error;
+    // %3A is '='
+    Pattern account_name_re = Pattern.compile("/register\\?q=inacct%3A([a-zA-Z0-9%]+)\"");
+    Pattern account_value_re = Pattern.compile(
+            "<span class=\"[^\"]*\\bamount\\b[^\"]*\">\\s*([-+]?[\\d.,]+)(?:\\s+(\\S+))?</span>");
+    Pattern tr_end_re = Pattern.compile("</tr>");
+    Pattern descriptions_line_re = Pattern.compile("\\bdescriptionsSuggester\\s*=\\s*new\\b");
+    Pattern description_items_re = Pattern.compile("\"value\":\"([^\"]+)\"");
     private boolean success;
-    public RetrieveTransactionsTask(WeakReference<TransactionListFragment> contextRef) {
+    public RetrieveTransactionsTask(WeakReference<MainActivity> contextRef) {
         this.contextRef = contextRef;
     }
     private static final void L(String msg) {
-//        Log.d("transaction-parser", msg);
+        Log.d("transaction-parser", msg);
     }
     @Override
     protected void onProgressUpdate(Progress... values) {
         super.onProgressUpdate(values);
-        TransactionListFragment context = getContext();
+        MainActivity context = getContext();
         if (context == null) return;
         context.onRetrieveProgress(values[0]);
     }
     @Override
     protected void onPreExecute() {
         super.onPreExecute();
-        TransactionListFragment context = getContext();
+        MainActivity context = getContext();
         if (context == null) return;
         context.onRetrieveStart();
     }
     @Override
     protected void onPostExecute(Void aVoid) {
         super.onPostExecute(aVoid);
-        TransactionListFragment context = getContext();
+        MainActivity context = getContext();
         if (context == null) return;
         context.onRetrieveDone(success);
     }
     @Override
     protected void onCancelled() {
         super.onCancelled();
-        TransactionListFragment context = getContext();
+        MainActivity context = getContext();
         if (context == null) return;
         context.onRetrieveDone(false);
     }
@@ -95,12 +109,15 @@ public class RetrieveTransactionsTask extends
         Progress progress = new Progress();
         int maxTransactionId = Progress.INDETERMINATE;
         success = false;
+        List<LedgerAccount> accountList = new ArrayList<>();
+        LedgerAccount lastAccount = null;
+        Data.backgroundTaskCount.incrementAndGet();
         try {
             HttpURLConnection http =
                     NetworkUtil.prepare_connection(params[0].getBackendPref(), "journal");
             http.setAllowUserInteraction(false);
             publishProgress(progress);
-            TransactionListFragment ctx = getContext();
+            MainActivity ctx = getContext();
             if (ctx == null) return null;
             try (SQLiteDatabase db = MLDB.getWritableDatabase()) {
                 try (InputStream resp = http.getInputStream()) {
@@ -109,8 +126,10 @@ public class RetrieveTransactionsTask extends
                     db.beginTransaction();
                     try {
                         db.execSQL("UPDATE transactions set keep=0");
+                        db.execSQL("update account_values set keep=0;");
+                        db.execSQL("update accounts set keep=0;");
 
-                        ParserState state = ParserState.EXPECTING_JOURNAL;
+                        ParserState state = ParserState.EXPECTING_ACCOUNT;
                         String line;
                         BufferedReader buf =
                                 new BufferedReader(new InputStreamReader(resp, "UTF-8"));
@@ -121,17 +140,61 @@ public class RetrieveTransactionsTask extends
                         LedgerTransaction transaction = null;
                         LINES:
                         while ((line = buf.readLine()) != null) {
-                            if (isCancelled()) break;
+                            throwIfCancelled();
                             Matcher m;
-                            //L(String.format("State is %d", state));
+                            //L(String.format("State is %d", updating));
                             switch (state) {
-                                case EXPECTING_JOURNAL:
-                                    if (!line.isEmpty() && (line.charAt(0) == ' ')) continue;
+                                case EXPECTING_ACCOUNT:
                                     if (line.equals("<h2>General Journal</h2>")) {
                                         state = ParserState.EXPECTING_TRANSACTION;
                                         L("→ expecting transaction");
+                                        Data.accounts.set(accountList);
+                                        continue;
+                                    }
+                                    m = account_name_re.matcher(line);
+                                    if (m.find()) {
+                                        String acct_encoded = m.group(1);
+                                        String acct_name = URLDecoder.decode(acct_encoded, "UTF-8");
+                                        acct_name = acct_name.replace("\"", "");
+                                        L(String.format("found account: %s", acct_name));
+
+                                        addAccount(db, acct_name);
+                                        lastAccount = new LedgerAccount(acct_name);
+                                        accountList.add(lastAccount);
+
+                                        state = ParserState.EXPECTING_ACCOUNT_AMOUNT;
+                                        L("→ expecting account amount");
+                                    }
+                                    break;
+
+                                case EXPECTING_ACCOUNT_AMOUNT:
+                                    m = account_value_re.matcher(line);
+                                    boolean match_found = false;
+                                    while (m.find()) {
+                                        throwIfCancelled();
+
+                                        match_found = true;
+                                        String value = m.group(1);
+                                        String currency = m.group(2);
+                                        if (currency == null) currency = "";
+                                        value = value.replace(',', '.');
+                                        L("curr=" + currency + ", value=" + value);
+                                        db.execSQL(
+                                                "insert or replace into account_values(account, currency, value, keep) values(?, ?, ?, 1);",
+                                                new Object[]{lastAccount.getName(),
+                                                             currency,
+                                                             Float.valueOf(value)
+                                                });
+                                        lastAccount.addAmount(Float.parseFloat(value), currency);
                                     }
+
+                                    if (match_found) {
+                                        state = ParserState.EXPECTING_ACCOUNT;
+                                        L("→ expecting account");
+                                    }
+
                                     break;
+
                                 case EXPECTING_TRANSACTION:
                                     if (!line.isEmpty() && (line.charAt(0) == ' ')) continue;
                                     m = transactionStartPattern.matcher(line);
@@ -139,7 +202,7 @@ public class RetrieveTransactionsTask extends
                                         transactionId = Integer.valueOf(m.group(1));
                                         state = ParserState.EXPECTING_TRANSACTION_DESCRIPTION;
                                         L(String.format(
-                                                "found transaction %d → expecting " + "description",
+                                                "found transaction %d → expecting description",
                                                 transactionId));
                                         progress.setProgress(++processedTransactionCount);
                                         if (maxTransactionId < transactionId)
@@ -151,11 +214,12 @@ public class RetrieveTransactionsTask extends
                                     }
                                     m = endPattern.matcher(line);
                                     if (m.find()) {
-                                        L("--- transaction list complete ---");
+                                        L("--- transaction value complete ---");
                                         success = true;
                                         break LINES;
                                     }
                                     break;
+
                                 case EXPECTING_TRANSACTION_DESCRIPTION:
                                     if (!line.isEmpty() && (line.charAt(0) == ' ')) continue;
                                     m = transactionDescriptionPattern.matcher(line);
@@ -174,6 +238,7 @@ public class RetrieveTransactionsTask extends
                                                 m.group(1), m.group(2)));
                                     }
                                     break;
+
                                 case EXPECTING_TRANSACTION_DETAILS:
                                     if (line.isEmpty()) {
                                         // transaction data collected
@@ -205,7 +270,7 @@ public class RetrieveTransactionsTask extends
 
                                         state = ParserState.EXPECTING_TRANSACTION;
                                         L(String.format(
-                                                "transaction %s saved → expecting " + "transaction",
+                                                "transaction %s saved → expecting transaction",
                                                 transaction.getId()));
 
 // sounds like a good idea, but transaction-1 may not be the first one chronologically
@@ -235,7 +300,7 @@ public class RetrieveTransactionsTask extends
                                     break;
                                 default:
                                     throw new RuntimeException(
-                                            String.format("Unknown parser state %s", state.name()));
+                                            String.format("Unknown parser updating %s", state.name()));
                             }
                         }
                         if (!isCancelled()) {
@@ -250,9 +315,9 @@ public class RetrieveTransactionsTask extends
             }
 
             if (success && !isCancelled()) {
-                Log.d("db", "Updating transaction list stamp");
+                Log.d("db", "Updating transaction value stamp");
                 MLDB.set_option_value(MLDB.OPT_TRANSACTION_LIST_STAMP, new Date().getTime());
-                ctx.model.reloadTransactions(ctx);
+                ((TransactionListViewModel)ctx.currentFragment.model).scheduleTransactionListReload(ctx);
             }
         }
         catch (MalformedURLException e) {
@@ -267,15 +332,32 @@ public class RetrieveTransactionsTask extends
             error = R.string.err_net_io_error;
             e.printStackTrace();
         }
+        finally {
+            Data.backgroundTaskCount.decrementAndGet();
+        }
         return null;
     }
-    TransactionListFragment getContext() {
+    private MainActivity getContext() {
         return contextRef.get();
     }
+    private void addAccount(SQLiteDatabase db, String name) {
+        do {
+            LedgerAccount acc = new LedgerAccount(name);
+            db.execSQL("update accounts set level = ?, keep = 1 where name = ?",
+                    new Object[]{acc.getLevel(), name});
+            db.execSQL("insert into accounts(name, name_upper, parent_name, level) select ?,?," +
+                       "?,? " + "where (select changes() = 0)",
+                    new Object[]{name, name.toUpperCase(), acc.getParentName(), acc.getLevel()});
+            name = acc.getParentName();
+        } while (name != null);
+    }
+    private void throwIfCancelled() {
+        if (isCancelled()) throw new OperationCanceledException(null);
+    }
 
     private enum ParserState {
-        EXPECTING_JOURNAL, EXPECTING_TRANSACTION, EXPECTING_TRANSACTION_DESCRIPTION,
-        EXPECTING_TRANSACTION_DETAILS
+        EXPECTING_ACCOUNT, EXPECTING_ACCOUNT_AMOUNT, EXPECTING_JOURNAL, EXPECTING_TRANSACTION,
+        EXPECTING_TRANSACTION_DESCRIPTION, EXPECTING_TRANSACTION_DETAILS
     }
 
     public static class Params {
diff --git a/app/src/main/java/net/ktnx/mobileledger/async/UpdateTransactionsTask.java b/app/src/main/java/net/ktnx/mobileledger/async/UpdateTransactionsTask.java
new file mode 100644 (file)
index 0000000..0509f35
--- /dev/null
@@ -0,0 +1,73 @@
+/*
+ * Copyright © 2019 Damyan Ivanov.
+ * This file is part of Mobile-Ledger.
+ * Mobile-Ledger 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.
+ *
+ * Mobile-Ledger 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 Mobile-Ledger. 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 android.util.Log;
+
+import net.ktnx.mobileledger.model.Data;
+import net.ktnx.mobileledger.model.LedgerTransaction;
+import net.ktnx.mobileledger.utils.MLDB;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class UpdateTransactionsTask extends AsyncTask<String, Void, List<LedgerTransaction>> {
+    protected List<LedgerTransaction> doInBackground(String[] filterAccName) {
+        Data.backgroundTaskCount.incrementAndGet();
+        try {
+            ArrayList<LedgerTransaction> newList = new ArrayList<>();
+
+            boolean hasFilter = (filterAccName != null) && (filterAccName.length > 0) &&
+                                (filterAccName[0] != null) && !filterAccName[0].isEmpty();
+
+            String sql;
+            String[] params;
+
+            sql = "SELECT id FROM transactions  ORDER BY date desc, id desc";
+            params = null;
+
+            if (hasFilter) {
+                sql = "SELECT distinct tr.id from transactions tr JOIN transaction_accounts ta " +
+                      "ON ta.transaction_id=tr.id WHERE ta.account_name LIKE ?||'%' AND ta" +
+                      ".amount <> 0 ORDER BY tr.date desc, tr.id desc";
+                params = filterAccName;
+            }
+
+            Log.d("tmp", sql);
+            try (SQLiteDatabase db = MLDB.getReadableDatabase()) {
+                try (Cursor cursor = db.rawQuery(sql, params)) {
+                    while (cursor.moveToNext()) {
+                        if (isCancelled()) return null;
+
+                        newList.add(new LedgerTransaction(cursor.getInt(0)));
+                    }
+                    Data.transactions.set(newList);
+                    Log.d("transactions", "transaction value updated");
+                }
+            }
+
+            return newList;
+        }
+        finally {
+            Data.backgroundTaskCount.decrementAndGet();
+        }
+    }
+}
diff --git a/app/src/main/java/net/ktnx/mobileledger/model/Data.java b/app/src/main/java/net/ktnx/mobileledger/model/Data.java
new file mode 100644 (file)
index 0000000..89d29c4
--- /dev/null
@@ -0,0 +1,27 @@
+/*
+ * Copyright © 2019 Damyan Ivanov.
+ * This file is part of Mobile-Ledger.
+ * Mobile-Ledger 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.
+ *
+ * Mobile-Ledger 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 Mobile-Ledger. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package net.ktnx.mobileledger.model;
+
+import java.util.List;
+
+public final class Data {
+    public static TransactionList transactions = new TransactionList();
+    public static ObservableValue<List<LedgerAccount>> accounts = new ObservableValue<>();
+    public static ObservableValue<List<String>> descriptions = new ObservableValue<>();
+    public static ObservableAtomicInteger backgroundTaskCount = new ObservableAtomicInteger(0);
+}
index 40d1f5fe05c4dc99cded5bf2a5790f35733c0caf..9db9f2995e03755c41eeb99c347e65d3509a1a36 100644 (file)
@@ -126,7 +126,7 @@ public class LedgerTransaction {
                 .rawQuery("SELECT 1 from transactions where data_hash = ?", new String[]{dataHash}))
         {
             boolean result = c.moveToFirst();
-            Log.d("transactions", String.format("Transaction %d (%s) %s", id, dataHash,
+            Log.d("db", String.format("Transaction %d (%s) %s", id, dataHash,
                     result ? "already present" : "not present"));
             return result;
         }
diff --git a/app/src/main/java/net/ktnx/mobileledger/model/ObservableAtomicInteger.java b/app/src/main/java/net/ktnx/mobileledger/model/ObservableAtomicInteger.java
new file mode 100644 (file)
index 0000000..79a2499
--- /dev/null
@@ -0,0 +1,140 @@
+/*
+ * Copyright © 2019 Damyan Ivanov.
+ * This file is part of Mobile-Ledger.
+ * Mobile-Ledger 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.
+ *
+ * Mobile-Ledger 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 Mobile-Ledger. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package net.ktnx.mobileledger.model;
+
+import android.os.Build;
+import android.support.annotation.RequiresApi;
+
+import java.util.Observable;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.function.IntBinaryOperator;
+import java.util.function.IntUnaryOperator;
+
+public class ObservableAtomicInteger extends Observable {
+    private AtomicInteger holder;
+    ObservableAtomicInteger() {
+        super();
+        holder = new AtomicInteger();
+    }
+    ObservableAtomicInteger(int initialValue) {
+        this();
+        holder.set(initialValue);
+    }
+    public int get() {
+        return holder.get();
+    }
+    public void set(int newValue) {
+        holder.set(newValue);
+        forceNotify();
+    }
+    private void forceNotify() {
+        setChanged();
+        notifyObservers();
+    }
+//    public void lazySet(int newValue) {
+//        holder.lazySet(newValue);
+//        forceNotify();
+//    }
+    public int getAndSet(int newValue) {
+        int result = holder.getAndSet(newValue);
+        forceNotify();
+        return result;
+    }
+    public boolean compareAndSet(int expect, int update) {
+        boolean result = holder.compareAndSet(expect, update);
+        if (result) forceNotify();
+        return result;
+    }
+//    public boolean weakCompareAndSet(int expect, int update) {
+//        boolean result = holder.weakCompareAndSet(expect, update);
+//        if (result) forceNotify();
+//        return result;
+//    }
+    public int getAndIncrement() {
+        int result = holder.getAndIncrement();
+        forceNotify();
+        return result;
+    }
+    public int getAndDecrement() {
+        int result = holder.getAndDecrement();
+        forceNotify();
+        return result;
+    }
+    public int getAndAdd(int delta) {
+        int result = holder.getAndAdd(delta);
+        forceNotify();
+        return result;
+    }
+    public int incrementAndGet() {
+        int result = holder.incrementAndGet();
+        forceNotify();
+        return result;
+    }
+    public int decrementAndGet() {
+        int result = holder.decrementAndGet();
+        forceNotify();
+        return result;
+    }
+    public int addAndGet(int delta) {
+        int result = holder.addAndGet(delta);
+        forceNotify();
+        return result;
+    }
+    @RequiresApi(Build.VERSION_CODES.N)
+    public int getAndUpdate(IntUnaryOperator updateFunction) {
+        int result = holder.getAndUpdate(updateFunction);
+        forceNotify();
+        return result;
+    }
+    @RequiresApi(api = Build.VERSION_CODES.N)
+    public int updateAndGet(IntUnaryOperator updateFunction) {
+        int result = holder.updateAndGet(updateFunction);
+        forceNotify();
+        return result;
+    }
+    @RequiresApi(api = Build.VERSION_CODES.N)
+    public int getAndAccumulate(int x, IntBinaryOperator accumulatorFunction) {
+        int result = holder.getAndAccumulate(x, accumulatorFunction);
+        forceNotify();
+        return result;
+    }
+    @RequiresApi(api = Build.VERSION_CODES.N)
+    public int accumulateAndGet(int x, IntBinaryOperator accumulatorFunction) {
+        int result = holder.accumulateAndGet(x, accumulatorFunction);
+        forceNotify();
+        return result;
+    }
+    public int intValue() {
+        return holder.intValue();
+    }
+    public long longValue() {
+        return holder.longValue();
+    }
+    public float floatValue() {
+        return holder.floatValue();
+    }
+    public double doubleValue() {
+        return holder.doubleValue();
+    }
+    public byte byteValue() {
+        return holder.byteValue();
+    }
+    public short shortValue() {
+        return holder.shortValue();
+    }
+}
diff --git a/app/src/main/java/net/ktnx/mobileledger/model/ObservableValue.java b/app/src/main/java/net/ktnx/mobileledger/model/ObservableValue.java
new file mode 100644 (file)
index 0000000..9a4bb27
--- /dev/null
@@ -0,0 +1,73 @@
+/*
+ * Copyright © 2019 Damyan Ivanov.
+ * This file is part of Mobile-Ledger.
+ * Mobile-Ledger 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.
+ *
+ * Mobile-Ledger 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 Mobile-Ledger. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package net.ktnx.mobileledger.model;
+
+import java.util.Observable;
+import java.util.Observer;
+
+public class ObservableValue<T> {
+    private final ObservableValueImpl<T> impl = new ObservableValueImpl<>();
+    public ObservableValue() {}
+    public ObservableValue(T initialValue) {
+        impl.setValue(initialValue, false);
+    }
+    public void set(T newValue) {
+        impl.setValue(newValue);
+    }
+    public T get() {
+        return impl.getValue();
+    }
+    public void addObserver(Observer o) {
+        impl.addObserver(o);
+    }
+    public void deleteObserver(Observer o) {
+        impl.deleteObserver(o);
+    }
+    public void notifyObservers() {
+        impl.notifyObservers();
+    }
+    public void notifyObservers(Object arg) {
+        impl.notifyObservers(arg);
+    }
+    public void deleteObservers() {
+        impl.deleteObservers();
+    }
+    public boolean hasChanged() {
+        return impl.hasChanged();
+    }
+    public int countObservers() {
+        return impl.countObservers();
+    }
+    private class ObservableValueImpl<T> extends Observable {
+        protected T value;
+        public void setValue(T newValue) {
+            setValue(newValue, true);
+        }
+        private synchronized void setValue(T newValue, boolean notify) {
+            if (newValue.equals(value)) return;
+
+            T oldValue = value;
+            value = newValue;
+            setChanged();
+            if (notify) notifyObservers(oldValue);
+        }
+        public T getValue() {
+            return value;
+        }
+    }
+}
\ No newline at end of file
diff --git a/app/src/main/java/net/ktnx/mobileledger/model/TransactionList.java b/app/src/main/java/net/ktnx/mobileledger/model/TransactionList.java
new file mode 100644 (file)
index 0000000..966f3cc
--- /dev/null
@@ -0,0 +1,23 @@
+/*
+ * Copyright © 2019 Damyan Ivanov.
+ * This file is part of Mobile-Ledger.
+ * Mobile-Ledger 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.
+ *
+ * Mobile-Ledger 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 Mobile-Ledger. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package net.ktnx.mobileledger.model;
+
+import java.util.List;
+
+public class TransactionList extends ObservableValue<List<LedgerTransaction>> {
+}
diff --git a/app/src/main/java/net/ktnx/mobileledger/ui/MobileLedgerListFragment.java b/app/src/main/java/net/ktnx/mobileledger/ui/MobileLedgerListFragment.java
new file mode 100644 (file)
index 0000000..fd21833
--- /dev/null
@@ -0,0 +1,34 @@
+/*
+ * Copyright © 2019 Damyan Ivanov.
+ * This file is part of Mobile-Ledger.
+ * Mobile-Ledger 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.
+ *
+ * Mobile-Ledger 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 Mobile-Ledger. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package net.ktnx.mobileledger.ui;
+
+import android.arch.lifecycle.ViewModel;
+import android.support.v4.app.Fragment;
+import android.support.v4.widget.SwipeRefreshLayout;
+import android.support.v7.widget.RecyclerView;
+
+import net.ktnx.mobileledger.ui.activity.MainActivity;
+import net.ktnx.mobileledger.ui.transaction_list.TransactionListAdapter;
+
+public class MobileLedgerListFragment extends Fragment {
+    public ViewModel model;
+    protected MainActivity mActivity;
+    public SwipeRefreshLayout swiper;
+    protected RecyclerView root;
+    public TransactionListAdapter modelAdapter;
+}
index 99cf91fecb07e32024878114560836dbbd90a0cf..d1241601809c09f5699e74924db6363b0f1cef63 100644 (file)
@@ -25,9 +25,6 @@ import android.preference.PreferenceManager;
 import android.support.annotation.NonNull;
 import android.support.annotation.Nullable;
 import android.support.design.widget.FloatingActionButton;
-import android.support.design.widget.Snackbar;
-import android.support.v4.app.Fragment;
-import android.support.v4.widget.SwipeRefreshLayout;
 import android.support.v7.widget.LinearLayoutManager;
 import android.support.v7.widget.RecyclerView;
 import android.util.Log;
@@ -41,6 +38,7 @@ import android.view.ViewGroup;
 import net.ktnx.mobileledger.R;
 import net.ktnx.mobileledger.async.RetrieveAccountsTask;
 import net.ktnx.mobileledger.model.LedgerAccount;
+import net.ktnx.mobileledger.ui.MobileLedgerListFragment;
 import net.ktnx.mobileledger.ui.RecyclerItemListener;
 import net.ktnx.mobileledger.ui.activity.MainActivity;
 import net.ktnx.mobileledger.utils.MLDB;
@@ -51,7 +49,7 @@ import java.util.List;
 
 import static net.ktnx.mobileledger.ui.activity.SettingsActivity.PREF_KEY_SHOW_ONLY_STARRED_ACCOUNTS;
 
-public class AccountSummaryFragment extends Fragment {
+public class AccountSummaryFragment extends MobileLedgerListFragment {
 
     private static long account_list_last_updated;
     private static boolean account_list_needs_update = true;
@@ -60,9 +58,7 @@ public class AccountSummaryFragment extends Fragment {
     private AccountSummaryViewModel model;
     private AccountSummaryAdapter modelAdapter;
     private Menu optMenu;
-    private MainActivity mActivity;
     private FloatingActionButton fab;
-    private SwipeRefreshLayout swiper;
     @Override
     public void onCreate(@Nullable Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
@@ -101,7 +97,7 @@ public class AccountSummaryFragment extends Fragment {
                 new RecyclerItemListener.RecyclerTouchListener() {
                     @Override
                     public void onClickItem(View v, int position) {
-                        Log.d("list", String.format("item %d clicked", position));
+                        Log.d("value", String.format("item %d clicked", position));
                         if (modelAdapter.isSelectionActive()) {
                             modelAdapter.selectItem(position);
                         }
@@ -117,7 +113,7 @@ public class AccountSummaryFragment extends Fragment {
 
                     @Override
                     public void onLongClickItem(View v, int position) {
-                        Log.d("list", String.format("item %d long-clicked", position));
+                        Log.d("value", String.format("item %d long-clicked", position));
                         modelAdapter.startSelection();
                         if (optMenu != null) {
                             optMenu.findItem(R.id.menu_acc_summary_cancel_selection)
@@ -168,34 +164,18 @@ public class AccountSummaryFragment extends Fragment {
     }
 
     private void update_accounts() {
-        RetrieveAccountsTask task = new RetrieveAccountsTask(new WeakReference<>(this));
+        RetrieveAccountsTask task = new RetrieveAccountsTask(new WeakReference<>(mActivity));
 
         task.setPref(PreferenceManager.getDefaultSharedPreferences(mActivity));
         task.execute();
 
     }
-    public void onAccountRefreshDone(int error) {
-        swiper.setRefreshing(false);
-        if (error != 0) {
-            String err_text = getResources().getString(error);
-            Log.d("visual", String.format("showing snackbar: %s", err_text));
-            Snackbar.make(swiper, err_text, Snackbar.LENGTH_LONG).show();
-        }
-        else {
-            MLDB.set_option_value(MLDB.OPT_LAST_REFRESH, new Date().getTime());
-            update_account_table();
-        }
-    }
     private void update_account_table() {
         if (this.getContext() == null) return;
 
         model.reloadAccounts(this.getContext());
         modelAdapter.notifyDataSetChanged();
     }
-    public void onRefreshAccountSummaryClicked(MenuItem mi) {
-        update_accounts(true);
-    }
-
     public void onShowOnlyStarredClicked(MenuItem mi) {
         SharedPreferences pref = PreferenceManager.getDefaultSharedPreferences(mActivity);
         boolean flag = pref.getBoolean(PREF_KEY_SHOW_ONLY_STARRED_ACCOUNTS, false);
index b98ccbe06678921a4cbdf04752842123cd3db89a..8d1b2f8b1a0936026a6eabf4dc89b1411430fde8 100644 (file)
@@ -21,8 +21,8 @@ import android.content.Intent;
 import android.content.pm.PackageInfo;
 import android.os.Build;
 import android.os.Bundle;
+import android.preference.PreferenceManager;
 import android.support.annotation.ColorInt;
-import android.support.v4.app.Fragment;
 import android.support.v4.app.FragmentManager;
 import android.support.v4.app.FragmentTransaction;
 import android.support.v4.view.GravityCompat;
@@ -35,14 +35,18 @@ import android.view.ContextMenu;
 import android.view.MenuItem;
 import android.view.View;
 import android.widget.LinearLayout;
+import android.widget.ProgressBar;
 import android.widget.TextView;
 
 import net.ktnx.mobileledger.R;
+import net.ktnx.mobileledger.async.RetrieveTransactionsTask;
 import net.ktnx.mobileledger.model.LedgerAccount;
+import net.ktnx.mobileledger.ui.MobileLedgerListFragment;
 import net.ktnx.mobileledger.ui.account_summary.AccountSummaryFragment;
 import net.ktnx.mobileledger.ui.transaction_list.TransactionListFragment;
 import net.ktnx.mobileledger.utils.MLDB;
 
+import java.lang.ref.WeakReference;
 import java.time.ZoneId;
 import java.time.format.DateTimeFormatter;
 import java.util.Date;
@@ -51,9 +55,13 @@ public class MainActivity extends AppCompatActivity {
     DrawerLayout drawer;
     private AccountSummaryFragment accountSummaryFragment;
     private TransactionListFragment transactionListFragment;
-    private Fragment currentFragment = null;
+    public MobileLedgerListFragment currentFragment = null;
     private FragmentManager fragmentManager;
     private TextView tvLastUpdate;
+    private RetrieveTransactionsTask retrieveTransactionsTask;
+    private View bTransactionListCancelDownload;
+    private ProgressBar progressBar;
+    private LinearLayout progressLayout;
 
     @Override
     protected void onCreate(Bundle savedInstanceState) {
@@ -85,6 +93,14 @@ public class MainActivity extends AppCompatActivity {
         long last_update = MLDB.get_option_value(MLDB.OPT_TRANSACTION_LIST_STAMP, 0L);
         Log.d("transactions", String.format("Last update = %d", last_update));
 
+        bTransactionListCancelDownload =
+                findViewById(R.id.transaction_list_cancel_download);
+        progressBar = findViewById(R.id.transaction_list_progress_bar);
+        if (progressBar == null)
+            throw new RuntimeException("Can't get hold on the transaction value progress bar");
+        progressLayout = findViewById(R.id.transaction_progress_layout);
+        if (progressLayout == null) throw new RuntimeException(
+                "Can't get hold on the transaction value progress bar layout");
 
         fragmentManager = getSupportFragmentManager();
 
@@ -237,5 +253,47 @@ public class MainActivity extends AppCompatActivity {
             }
         }
     }
+    public void update_transactions() {
+        retrieveTransactionsTask = new RetrieveTransactionsTask(new WeakReference<>(this));
+
+        RetrieveTransactionsTask.Params params = new RetrieveTransactionsTask.Params(
+                PreferenceManager.getDefaultSharedPreferences(this));
+
+        retrieveTransactionsTask.execute(params);
+        bTransactionListCancelDownload.setEnabled(true);
+    }
+    public void onStopTransactionRefreshClick(View view) {
+        Log.d("interactive", "Cancelling transactions refresh");
+        if (retrieveTransactionsTask != null) retrieveTransactionsTask.cancel(false);
+        bTransactionListCancelDownload.setEnabled(false);
+    }
+    public void onRetrieveDone(boolean success) {
+        progressLayout.setVisibility(View.GONE);
+        updateLastUpdateText();
+    }
+    public void onRetrieveStart() {
+        progressBar.setIndeterminate(true);
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) progressBar.setProgress(0, false);
+        else progressBar.setProgress(0);
+        progressLayout.setVisibility(View.VISIBLE);
+    }
+    public void onRetrieveProgress(RetrieveTransactionsTask.Progress progress) {
+        if ((progress.getTotal() == RetrieveTransactionsTask.Progress.INDETERMINATE) ||
+            (progress.getTotal() == 0))
+        {
+            progressBar.setIndeterminate(true);
+        }
+        else {
+            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+                progressBar.setMin(0);
+            }
+            progressBar.setMax(progress.getTotal());
+            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
+                progressBar.setProgress(progress.getProgress(), true);
+            }
+            else progressBar.setProgress(progress.getProgress());
+            progressBar.setIndeterminate(false);
+        }
+    }
 
 }
index 5b1e46f4b5ea3e05e3737c2a37457dca063c9e27..807c04f119b08729b9906eb36a9884f3f5194619 100644 (file)
@@ -20,8 +20,8 @@ package net.ktnx.mobileledger.ui.transaction_list;
 import android.content.Context;
 import android.database.sqlite.SQLiteDatabase;
 import android.graphics.Typeface;
+import android.os.AsyncTask;
 import android.support.annotation.NonNull;
-import android.support.constraint.ConstraintLayout;
 import android.support.v7.widget.AppCompatTextView;
 import android.support.v7.widget.RecyclerView;
 import android.util.Log;
@@ -40,92 +40,20 @@ import net.ktnx.mobileledger.utils.MLDB;
 
 import static net.ktnx.mobileledger.utils.DimensionUtils.dp2px;
 
-public class TransactionListAdapter
-        extends RecyclerView.Adapter<TransactionListAdapter.TransactionRowHolder> {
-    TransactionListViewModel model;
+public class TransactionListAdapter extends RecyclerView.Adapter<TransactionRowHolder> {
     private String boldAccountName;
-    public TransactionListAdapter(TransactionListViewModel model) {
-        this.model = model;
-    }
     public void onBindViewHolder(@NonNull TransactionRowHolder holder, int position) {
-        LedgerTransaction tr = model.getTransaction(position);
-        // in a race when transaction list is reduced, but the model hasn't been notified yet
+        LedgerTransaction tr = TransactionListViewModel.getTransaction(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
         // the out-of-range get() call nothing bad happens - just a to-be-deleted view remains
         // a bit longer
         if (tr == null) return;
 
-        Context ctx = holder.row.getContext();
-
-        try (SQLiteDatabase db = MLDB.getReadableDatabase()) {
-            tr.loadData(db);
-            holder.tvDescription.setText(tr.getDescription());
-            holder.tvDate.setText(tr.getDate());
-
-            int rowIndex = 0;
-            for (LedgerTransactionAccount acc : tr.getAccounts()) {
-                LinearLayout row = (LinearLayout) holder.tableAccounts.getChildAt(rowIndex++);
-                TextView accName, accAmount;
-                if (row == null) {
-                    row = new LinearLayout(ctx);
-                    row.setLayoutParams(
-                            new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT,
-                                    LinearLayout.LayoutParams.WRAP_CONTENT));
-                    row.setGravity(Gravity.CENTER_VERTICAL);
-                    row.setOrientation(LinearLayout.HORIZONTAL);
-                    row.setPaddingRelative(dp2px(ctx, 8), 0, 0, 0);
-                    accName = new AppCompatTextView(ctx);
-                    accName.setLayoutParams(
-                            new LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT,
-                                    5f));
-                    accName.setTextAlignment(View.TEXT_ALIGNMENT_VIEW_START);
-                    row.addView(accName);
-                    accAmount = new AppCompatTextView(ctx);
-                    LinearLayout.LayoutParams llp =
-                            new LinearLayout.LayoutParams(LinearLayout.LayoutParams.WRAP_CONTENT,
-                                    LinearLayout.LayoutParams.WRAP_CONTENT);
-                    llp.setMarginEnd(0);
-                    accAmount.setLayoutParams(llp);
-                    accAmount.setTextAlignment(View.TEXT_ALIGNMENT_VIEW_END);
-                    accAmount.setMinWidth(dp2px(ctx, 60));
-                    row.addView(accAmount);
-                    holder.tableAccounts.addView(row);
-                }
-                else {
-                    accName = (TextView) row.getChildAt(0);
-                    accAmount = (TextView) row.getChildAt(1);
-                }
-                accName.setText(acc.getAccountName());
-                accAmount.setText(acc.toString());
-
-                if ((boldAccountName != null) && boldAccountName.equals(acc.getAccountName())) {
-                    accName.setTypeface(null, Typeface.BOLD);
-                    accAmount.setTypeface(null, Typeface.BOLD);
-                    accName.setTextColor(Globals.primaryDark);
-                    accAmount.setTextColor(Globals.primaryDark);
-                }
-                else {
-                    accName.setTypeface(null, Typeface.NORMAL);
-                    accAmount.setTypeface(null, Typeface.NORMAL);
-                    accName.setTextColor(Globals.defaultTextColor);
-                    accAmount.setTextColor(Globals.defaultTextColor);
-                }
-
-            }
-            if (holder.tableAccounts.getChildCount() > rowIndex) {
-                holder.tableAccounts
-                        .removeViews(rowIndex, holder.tableAccounts.getChildCount() - rowIndex);
-            }
-
-            if (position % 2 == 0) {
-                holder.row.setBackgroundColor(Globals.table_row_even_bg);
-            }
-            else {
-                holder.row.setBackgroundColor(Globals.table_row_odd_bg);
-            }
+        Log.d("transactions", String.format("Filling position %d", position));
 
-            Log.d("transactions", String.format("Filled position %d", position));
-        }
+        TransactionLoader loader = new TransactionLoader();
+        loader.execute(new TransactionLoaderParams(tr, holder, position, boldAccountName));
     }
 
     @NonNull
@@ -139,7 +67,7 @@ public class TransactionListAdapter
 
     @Override
     public int getItemCount() {
-        return model.getTransactionCount();
+        return TransactionListViewModel.getTransactionCount();
     }
     public void setBoldAccountName(String boldAccountName) {
         this.boldAccountName = boldAccountName;
@@ -147,16 +75,126 @@ public class TransactionListAdapter
     public void resetBoldAccountName() {
         this.boldAccountName = null;
     }
-    class TransactionRowHolder extends RecyclerView.ViewHolder {
-        TextView tvDescription, tvDate;
-        LinearLayout tableAccounts;
-        ConstraintLayout row;
-        public TransactionRowHolder(@NonNull View itemView) {
-            super(itemView);
-            this.row = itemView.findViewById(R.id.transaction_row);
-            this.tvDescription = itemView.findViewById(R.id.transaction_row_description);
-            this.tvDate = itemView.findViewById(R.id.transaction_row_date);
-            this.tableAccounts = itemView.findViewById(R.id.transaction_row_acc_amounts);
+
+    enum LoaderStep {HEAD, ACCOUNTS, DONE}
+
+    private static class TransactionLoader
+            extends AsyncTask<TransactionLoaderParams, TransactionLoaderStep, Void> {
+        @Override
+        protected Void doInBackground(TransactionLoaderParams... p) {
+            LedgerTransaction tr = p[0].transaction;
+
+            try (SQLiteDatabase db = MLDB.getReadableDatabase()) {
+                tr.loadData(db);
+
+                publishProgress(new TransactionLoaderStep(p[0].holder, p[0].position, tr));
+
+                int rowIndex = 0;
+                for (LedgerTransactionAccount acc : tr.getAccounts()) {
+                    publishProgress(new TransactionLoaderStep(p[0].holder, acc, rowIndex++,
+                            p[0].boldAccountName));
+                }
+
+                publishProgress(new TransactionLoaderStep(p[0].holder, p[0].position, rowIndex));
+            }
+            return null;
+        }
+        @Override
+        protected void onProgressUpdate(TransactionLoaderStep... values) {
+            super.onProgressUpdate(values);
+            TransactionLoaderStep step = values[0];
+            TransactionRowHolder holder = step.getHolder();
+
+            switch (step.getStep()) {
+                case HEAD:
+                    holder.tvDescription.setText(step.getTransaction().getDescription());
+                    holder.tvDate.setText(step.getTransaction().getDate());
+
+                    if (step.getPosition() % 2 == 0) {
+                        holder.row.setBackgroundColor(Globals.table_row_even_bg);
+                    }
+                    else {
+                        holder.row.setBackgroundColor(Globals.table_row_odd_bg);
+                    }
+
+                    break;
+                case ACCOUNTS:
+                    int rowIndex = step.getAccountPosition();
+                    Context ctx = holder.row.getContext();
+                    LinearLayout row = (LinearLayout) holder.tableAccounts.getChildAt(rowIndex);
+                    TextView accName, accAmount;
+                    if (row == null) {
+                        row = new LinearLayout(ctx);
+                        row.setLayoutParams(new LinearLayout.LayoutParams(
+                                LinearLayout.LayoutParams.MATCH_PARENT,
+                                LinearLayout.LayoutParams.WRAP_CONTENT));
+                        row.setGravity(Gravity.CENTER_VERTICAL);
+                        row.setOrientation(LinearLayout.HORIZONTAL);
+                        row.setPaddingRelative(dp2px(ctx, 8), 0, 0, 0);
+                        accName = new AppCompatTextView(ctx);
+                        accName.setLayoutParams(new LinearLayout.LayoutParams(0,
+                                LinearLayout.LayoutParams.WRAP_CONTENT, 5f));
+                        accName.setTextAlignment(View.TEXT_ALIGNMENT_VIEW_START);
+                        row.addView(accName);
+                        accAmount = new AppCompatTextView(ctx);
+                        LinearLayout.LayoutParams llp = new LinearLayout.LayoutParams(
+                                LinearLayout.LayoutParams.WRAP_CONTENT,
+                                LinearLayout.LayoutParams.WRAP_CONTENT);
+                        llp.setMarginEnd(0);
+                        accAmount.setLayoutParams(llp);
+                        accAmount.setTextAlignment(View.TEXT_ALIGNMENT_VIEW_END);
+                        accAmount.setMinWidth(dp2px(ctx, 60));
+                        row.addView(accAmount);
+                        holder.tableAccounts.addView(row);
+                    }
+                    else {
+                        accName = (TextView) row.getChildAt(0);
+                        accAmount = (TextView) row.getChildAt(1);
+                    }
+                    LedgerTransactionAccount acc = step.getAccount();
+
+                    accName.setText(acc.getAccountName());
+                    accAmount.setText(acc.toString());
+
+                    String boldAccountName = step.getBoldAccountName();
+                    if ((boldAccountName != null) && boldAccountName.equals(acc.getAccountName())) {
+                        accName.setTypeface(null, Typeface.BOLD);
+                        accAmount.setTypeface(null, Typeface.BOLD);
+                        accName.setTextColor(Globals.primaryDark);
+                        accAmount.setTextColor(Globals.primaryDark);
+                    }
+                    else {
+                        accName.setTypeface(null, Typeface.NORMAL);
+                        accAmount.setTypeface(null, Typeface.NORMAL);
+                        accName.setTextColor(Globals.defaultTextColor);
+                        accAmount.setTextColor(Globals.defaultTextColor);
+                    }
+
+                    break;
+                case DONE:
+                    int accCount = step.getAccountCount();
+                    if (holder.tableAccounts.getChildCount() > accCount) {
+                        holder.tableAccounts.removeViews(accCount,
+                                holder.tableAccounts.getChildCount() - accCount);
+                    }
+
+                    Log.d("transactions",
+                            String.format("Position %d fill done", step.getPosition()));
+            }
+        }
+    }
+
+    private class TransactionLoaderParams {
+        LedgerTransaction transaction;
+        TransactionRowHolder holder;
+        int position;
+        String boldAccountName;
+        TransactionLoaderParams(LedgerTransaction transaction, TransactionRowHolder holder,
+                                int position, String boldAccountName) {
+            this.transaction = transaction;
+            this.holder = holder;
+            this.position = position;
+            this.boldAccountName = boldAccountName;
         }
     }
 }
\ No newline at end of file
index f8738a59d10810c0d15c463e792ce93a9338add3..c84da7921b6ab343b2dbaed5033fd897e00d8466 100644 (file)
@@ -20,14 +20,10 @@ package net.ktnx.mobileledger.ui.transaction_list;
 import android.arch.lifecycle.ViewModelProviders;
 import android.content.Context;
 import android.database.MatrixCursor;
-import android.os.Build;
 import android.os.Bundle;
-import android.preference.PreferenceManager;
 import android.support.annotation.NonNull;
 import android.support.annotation.Nullable;
 import android.support.design.widget.FloatingActionButton;
-import android.support.v4.app.Fragment;
-import android.support.v4.widget.SwipeRefreshLayout;
 import android.support.v7.widget.LinearLayoutManager;
 import android.support.v7.widget.RecyclerView;
 import android.util.Log;
@@ -40,34 +36,27 @@ import android.view.ViewGroup;
 import android.view.inputmethod.InputMethodManager;
 import android.widget.AdapterView;
 import android.widget.AutoCompleteTextView;
-import android.widget.LinearLayout;
-import android.widget.ProgressBar;
 
 import net.ktnx.mobileledger.R;
-import net.ktnx.mobileledger.async.RetrieveTransactionsTask;
+import net.ktnx.mobileledger.model.Data;
+import net.ktnx.mobileledger.ui.MobileLedgerListFragment;
 import net.ktnx.mobileledger.ui.activity.MainActivity;
 import net.ktnx.mobileledger.utils.Globals;
 import net.ktnx.mobileledger.utils.MLDB;
 
-import java.lang.ref.WeakReference;
+import java.util.Observable;
+import java.util.Observer;
 
 import static android.content.Context.INPUT_METHOD_SERVICE;
 
-public class TransactionListFragment extends Fragment {
+public class TransactionListFragment extends MobileLedgerListFragment {
     public static final String BUNDLE_KEY_FILTER_ACCOUNT_NAME = "filter_account_name";
-    public TransactionListViewModel model;
     private String mShowOnlyAccountName;
-    private MainActivity mActivity;
-    private View bTransactionListCancelDownload;
     private MenuItem menuTransactionListFilter;
     private View vAccountFilter;
-    private SwipeRefreshLayout swiper;
-    private RecyclerView root;
-    private ProgressBar progressBar;
-    private LinearLayout progressLayout;
-    private TransactionListAdapter modelAdapter;
-    private RetrieveTransactionsTask retrieveTransactionsTask;
     private AutoCompleteTextView accNameFilter;
+    private static void update(Observable o, Object arg) {
+    }
     public void setShowOnlyAccountName(String mShowOnlyAccountName) {
         this.mShowOnlyAccountName = mShowOnlyAccountName;
         if (modelAdapter != null) {
@@ -114,15 +103,10 @@ public class TransactionListFragment extends Fragment {
         swiper = mActivity.findViewById(R.id.transaction_swipe);
         if (swiper == null) throw new RuntimeException("Can't get hold on the swipe layout");
         root = mActivity.findViewById(R.id.transaction_root);
-        if (root == null) throw new RuntimeException("Can't get hold on the transaction list view");
-        progressBar = mActivity.findViewById(R.id.transaction_list_progress_bar);
-        if (progressBar == null)
-            throw new RuntimeException("Can't get hold on the transaction list progress bar");
-        progressLayout = mActivity.findViewById(R.id.transaction_progress_layout);
-        if (progressLayout == null) throw new RuntimeException(
-                "Can't get hold on the transaction list progress bar layout");
+        if (root == null)
+            throw new RuntimeException("Can't get hold on the transaction value view");
         model = ViewModelProviders.of(this).get(TransactionListViewModel.class);
-        modelAdapter = new TransactionListAdapter(model);
+        modelAdapter = new TransactionListAdapter();
 
         modelAdapter.setBoldAccountName(mShowOnlyAccountName);
 
@@ -149,15 +133,13 @@ public class TransactionListFragment extends Fragment {
 
         swiper.setOnRefreshListener(() -> {
             Log.d("ui", "refreshing transactions via swipe");
-            update_transactions();
+            mActivity.update_transactions();
         });
 
         swiper.setColorSchemeResources(R.color.colorPrimary, R.color.colorAccent);
 
         vAccountFilter = mActivity.findViewById(R.id.transaction_list_account_name_filter);
         accNameFilter = mActivity.findViewById(R.id.transaction_filter_account_name);
-        bTransactionListCancelDownload =
-                mActivity.findViewById(R.id.transaction_list_cancel_download);
 
         TransactionListFragment me = this;
         MLDB.hook_autocompletion_adapter(mActivity, accNameFilter, "accounts", "name");
@@ -165,7 +147,7 @@ public class TransactionListFragment extends Fragment {
             @Override
             public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
                 Log.d("tmp", "direct onItemClick");
-                model.reloadTransactions(me);
+                ((TransactionListViewModel) model).scheduleTransactionListReload(mActivity);
                 MatrixCursor mc = (MatrixCursor) parent.getItemAtPosition(position);
                 modelAdapter.setBoldAccountName(mc.getString(1));
                 modelAdapter.notifyDataSetChanged();
@@ -179,7 +161,25 @@ public class TransactionListFragment extends Fragment {
             Log.d("flow", String.format("Account filter set to '%s'", mShowOnlyAccountName));
         }
 
-        model.reloadTransactions(this);
+        TransactionListViewModel.scheduleTransactionListReload(mActivity);
+        TransactionListViewModel.updating.addObserver(new Observer() {
+            @Override
+            public void update(Observable o, Object arg) {
+                swiper.setRefreshing(TransactionListViewModel.updating.get());
+            }
+        });
+
+        Data.transactions.addObserver(new Observer() {
+            @Override
+            public void update(Observable o, Object arg) {
+                mActivity.runOnUiThread(new Runnable() {
+                    @Override
+                    public void run() {
+                        modelAdapter.notifyDataSetChanged();
+                    }
+                });
+            }
+        });
     }
     @Override
     public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
@@ -194,57 +194,14 @@ public class TransactionListFragment extends Fragment {
 
         super.onCreateOptionsMenu(menu, inflater);
     }
-    private void update_transactions() {
-        retrieveTransactionsTask = new RetrieveTransactionsTask(new WeakReference<>(this));
-
-        RetrieveTransactionsTask.Params params = new RetrieveTransactionsTask.Params(
-                PreferenceManager.getDefaultSharedPreferences(mActivity));
-
-        retrieveTransactionsTask.execute(params);
-        bTransactionListCancelDownload.setEnabled(true);
-    }
-    public void onRetrieveStart() {
-        progressBar.setIndeterminate(true);
-        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) progressBar.setProgress(0, false);
-        else progressBar.setProgress(0);
-        progressLayout.setVisibility(View.VISIBLE);
-    }
-    public void onRetrieveProgress(RetrieveTransactionsTask.Progress progress) {
-        if ((progress.getTotal() == RetrieveTransactionsTask.Progress.INDETERMINATE) ||
-            (progress.getTotal() == 0))
-        {
-            progressBar.setIndeterminate(true);
-        }
-        else {
-            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
-                progressBar.setMin(0);
-            }
-            progressBar.setMax(progress.getTotal());
-            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
-                progressBar.setProgress(progress.getProgress(), true);
-            }
-            else progressBar.setProgress(progress.getProgress());
-            progressBar.setIndeterminate(false);
-        }
-    }
 
-    public void onRetrieveDone(boolean success) {
-        progressLayout.setVisibility(View.GONE);
-        swiper.setRefreshing(false);
-        mActivity.updateLastUpdateText();
-        if (success) {
-            Log.d("transactions", "calling notifyDataSetChanged()");
-            modelAdapter.notifyDataSetChanged();
-        }
-    }
     public void onClearAccountNameClick(View view) {
         vAccountFilter.setVisibility(View.GONE);
         if (menuTransactionListFilter != null) menuTransactionListFilter.setVisible(true);
         accNameFilter.setText(null);
         mShowOnlyAccountName = null;
-        model.reloadTransactions(this);
         modelAdapter.resetBoldAccountName();
-        modelAdapter.notifyDataSetChanged();
+        TransactionListViewModel.scheduleTransactionListReload(mActivity);
         Globals.hideSoftKeyboard(mActivity);
     }
     public void onShowFilterClick(MenuItem menuItem) {
@@ -257,9 +214,4 @@ public class TransactionListFragment extends Fragment {
             imm.showSoftInput(accNameFilter, 0);
         }
     }
-    public void onStopTransactionRefreshClick(View view) {
-        Log.d("interactive", "Cancelling transactions refresh");
-        if (retrieveTransactionsTask != null) retrieveTransactionsTask.cancel(false);
-        bTransactionListCancelDownload.setEnabled(false);
-    }
-}
+}
\ No newline at end of file
index 52fb19761d19a6adcda5bdd14643873ffcbed927..f1c6df01348e000947d75fb5f314ea60014de7c4 100644 (file)
@@ -19,68 +19,58 @@ package net.ktnx.mobileledger.ui.transaction_list;
 
 import android.app.Activity;
 import android.arch.lifecycle.ViewModel;
-import android.database.Cursor;
-import android.database.sqlite.SQLiteDatabase;
-import android.util.Log;
+import android.os.AsyncTask;
 import android.view.View;
 import android.widget.AutoCompleteTextView;
 
 import net.ktnx.mobileledger.R;
+import net.ktnx.mobileledger.async.UpdateTransactionsTask;
+import net.ktnx.mobileledger.model.Data;
 import net.ktnx.mobileledger.model.LedgerTransaction;
-import net.ktnx.mobileledger.utils.MLDB;
+import net.ktnx.mobileledger.model.ObservableValue;
 
-import java.util.ArrayList;
+import java.util.List;
 
 public class TransactionListViewModel extends ViewModel {
+    public static ObservableValue<Boolean> updating = new ObservableValue<>();
 
-    private ArrayList<LedgerTransaction> transactions;
-
-    public void reloadTransactions(TransactionListFragment context) {
-        ArrayList<LedgerTransaction> newList = new ArrayList<>();
-
-        Activity act = context.getActivity();
-
+    public static void scheduleTransactionListReload(Activity act) {
         boolean hasFilter =
                 act.findViewById(R.id.transaction_list_account_name_filter).getVisibility() ==
                 View.VISIBLE;
-
-        String sql;
-        String[] params;
-
-        sql = "SELECT id FROM transactions  ORDER BY date desc, id desc";
-        params = null;
-
-        if (hasFilter) {
-            String filterAccName = String.valueOf(
-                    ((AutoCompleteTextView) act.findViewById(R.id.transaction_filter_account_name))
-                            .getText());
-
-            if (!filterAccName.isEmpty()) {
-                sql = "SELECT distinct tr.id from transactions tr JOIN transaction_accounts ta " +
-                      "ON ta.transaction_id=tr.id WHERE ta.account_name LIKE ?||'%' AND ta" +
-                      ".amount <> 0 ORDER BY tr.date desc, tr.id desc";
-                params = new String[]{filterAccName};
-            }
-        }
-
-        Log.d("tmp", sql);
-        try (SQLiteDatabase db = MLDB.getReadableDatabase()) {
-            try (Cursor cursor = db.rawQuery(sql, params)) {
-                while (cursor.moveToNext()) {
-                    newList.add(new LedgerTransaction(cursor.getInt(0)));
-                }
-                transactions = newList;
-                Log.d("transactions", "transaction list updated");
-            }
-        }
-
+        String accFilter = hasFilter ? String.valueOf(
+                ((AutoCompleteTextView) act.findViewById(R.id.transaction_filter_account_name))
+                        .getText()) : null;
+        updating.set(true);
+        AsyncTask<String, Void, List<LedgerTransaction>> task = new UTT();
+        task.execute(accFilter);
     }
-    public LedgerTransaction getTransaction(int position) {
+    public static LedgerTransaction getTransaction(int position) {
+        List<LedgerTransaction> transactions = Data.transactions.get();
         if (position >= transactions.size()) return null;
         return transactions.get(position);
     }
-    public int getTransactionCount() {
+    public static int getTransactionCount() {
+        List<LedgerTransaction> transactions = Data.transactions.get();
         if (transactions == null) return 0;
         return transactions.size();
     }
+    private static class UTT extends UpdateTransactionsTask {
+        @Override
+        protected void onPostExecute(List<LedgerTransaction> list) {
+            super.onPostExecute(list);
+            updating.set(false);
+            if (list != null) Data.transactions.set(list);
+        }
+        @Override
+        protected void onCancelled(List<LedgerTransaction> ledgerTransactions) {
+            super.onCancelled(ledgerTransactions);
+            updating.set(false);
+        }
+        @Override
+        protected void onCancelled() {
+            super.onCancelled();
+            updating.set(false);
+        }
+    }
 }
diff --git a/app/src/main/java/net/ktnx/mobileledger/ui/transaction_list/TransactionLoaderStep.java b/app/src/main/java/net/ktnx/mobileledger/ui/transaction_list/TransactionLoaderStep.java
new file mode 100644 (file)
index 0000000..e524358
--- /dev/null
@@ -0,0 +1,77 @@
+/*
+ * Copyright © 2019 Damyan Ivanov.
+ * This file is part of Mobile-Ledger.
+ * Mobile-Ledger 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.
+ *
+ * Mobile-Ledger 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 Mobile-Ledger. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package net.ktnx.mobileledger.ui.transaction_list;
+
+import net.ktnx.mobileledger.model.LedgerTransaction;
+import net.ktnx.mobileledger.model.LedgerTransactionAccount;
+
+class TransactionLoaderStep {
+    private int position;
+    private int accountCount;
+    private TransactionListAdapter.LoaderStep step;
+    private TransactionRowHolder holder;
+    private LedgerTransaction transaction;
+    private LedgerTransactionAccount account;
+    private int accountPosition;
+    private String boldAccountName;
+    public TransactionLoaderStep(TransactionRowHolder holder, int position,
+                                 LedgerTransaction transaction) {
+        this.step = TransactionListAdapter.LoaderStep.HEAD;
+        this.holder = holder;
+        this.transaction = transaction;
+        this.position = position;
+    }
+    public TransactionLoaderStep(TransactionRowHolder holder, LedgerTransactionAccount account,
+                                 int accountPosition, String boldAccountName) {
+        this.step = TransactionListAdapter.LoaderStep.ACCOUNTS;
+        this.holder = holder;
+        this.account = account;
+        this.accountPosition = accountPosition;
+        this.boldAccountName = boldAccountName;
+    }
+    public TransactionLoaderStep(TransactionRowHolder holder, int position, int accountCount) {
+        this.step = TransactionListAdapter.LoaderStep.DONE;
+        this.holder = holder;
+        this.position = position;
+        this.accountCount = accountCount;
+    }
+    public int getAccountCount() {
+        return accountCount;
+    }
+    public int getPosition() {
+        return position;
+    }
+    public String getBoldAccountName() {
+        return boldAccountName;
+    }
+    public int getAccountPosition() {
+        return accountPosition;
+    }
+    public TransactionRowHolder getHolder() {
+        return holder;
+    }
+    public TransactionListAdapter.LoaderStep getStep() {
+        return step;
+    }
+    public LedgerTransaction getTransaction() {
+        return transaction;
+    }
+    public LedgerTransactionAccount getAccount() {
+        return account;
+    }
+}
diff --git a/app/src/main/java/net/ktnx/mobileledger/ui/transaction_list/TransactionRowHolder.java b/app/src/main/java/net/ktnx/mobileledger/ui/transaction_list/TransactionRowHolder.java
new file mode 100644 (file)
index 0000000..5e97596
--- /dev/null
@@ -0,0 +1,40 @@
+/*
+ * Copyright © 2019 Damyan Ivanov.
+ * This file is part of Mobile-Ledger.
+ * Mobile-Ledger 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.
+ *
+ * Mobile-Ledger 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 Mobile-Ledger. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package net.ktnx.mobileledger.ui.transaction_list;
+
+import android.support.annotation.NonNull;
+import android.support.constraint.ConstraintLayout;
+import android.support.v7.widget.RecyclerView;
+import android.view.View;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+import net.ktnx.mobileledger.R;
+
+class TransactionRowHolder extends RecyclerView.ViewHolder {
+    TextView tvDescription, tvDate;
+    LinearLayout tableAccounts;
+    ConstraintLayout row;
+    public TransactionRowHolder(@NonNull View itemView) {
+        super(itemView);
+        this.row = itemView.findViewById(R.id.transaction_row);
+        this.tvDescription = itemView.findViewById(R.id.transaction_row_description);
+        this.tvDate = itemView.findViewById(R.id.transaction_row_date);
+        this.tableAccounts = itemView.findViewById(R.id.transaction_row_acc_amounts);
+    }
+}