working transaction list retrieval
authorDamyan Ivanov <dam+mobileledger@ktnx.net>
Sat, 15 Dec 2018 13:38:27 +0000 (13:38 +0000)
committerDamyan Ivanov <dam+mobileledger@ktnx.net>
Sat, 15 Dec 2018 13:38:27 +0000 (13:38 +0000)
13 files changed:
app/src/main/java/net/ktnx/mobileledger/TransactionListActivity.java
app/src/main/java/net/ktnx/mobileledger/TransactionListAdapter.java [new file with mode: 0644]
app/src/main/java/net/ktnx/mobileledger/async/RetrieveTransactionsTask.java
app/src/main/java/net/ktnx/mobileledger/model/LedgerTransaction.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/utils/Digest.java [new file with mode: 0644]
app/src/main/java/net/ktnx/mobileledger/utils/MobileLedgerDatabase.java
app/src/main/res/layout/transaction_list_activity.xml
app/src/main/res/layout/transaction_list_fragment.xml
app/src/main/res/layout/transaction_list_row.xml [new file with mode: 0644]
app/src/main/res/raw/sql_8.sql [new file with mode: 0644]
app/src/main/res/values/strings.xml

index a948715e98dc3bf9134302ffae7b83c21c4e68cb..1dc5f026d900cb9961805e5465855dbc93febf40 100644 (file)
 
 package net.ktnx.mobileledger;
 
+import android.arch.lifecycle.ViewModelProviders;
+import android.os.Build;
 import android.os.Bundle;
+import android.preference.PreferenceManager;
+import android.support.v4.widget.SwipeRefreshLayout;
 import android.support.v7.app.ActionBar;
 import android.support.v7.app.AppCompatActivity;
+import android.support.v7.widget.LinearLayoutManager;
+import android.support.v7.widget.RecyclerView;
 import android.support.v7.widget.Toolbar;
 import android.util.Log;
+import android.view.View;
+import android.widget.ProgressBar;
+import android.widget.TextView;
 
-import net.ktnx.mobileledger.ui.transaction_list.TransactionListFragment;
+import net.ktnx.mobileledger.async.RetrieveTransactionsTask;
+import net.ktnx.mobileledger.model.LedgerTransaction;
+import net.ktnx.mobileledger.ui.transaction_list.TransactionListViewModel;
+import net.ktnx.mobileledger.utils.MobileLedgerDatabase;
 
-public class TransactionListActivity extends AppCompatActivity {
+import java.lang.ref.WeakReference;
+import java.time.ZoneId;
+import java.time.format.DateTimeFormatter;
+import java.util.Date;
+import java.util.List;
 
+public class TransactionListActivity extends AppCompatActivity {
+    private SwipeRefreshLayout swiper;
+    private RecyclerView root;
+    private ProgressBar progressBar;
+    private TransactionListViewModel model;
+    private TextView tvLastUpdate;
+    private TransactionListAdapter modelAdapter;
     @Override
     protected void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
         setContentView(R.layout.transaction_list_activity);
-        if (savedInstanceState == null) {
-            getSupportFragmentManager().beginTransaction()
-                    .replace(R.id.container, TransactionListFragment.newInstance()).commitNow();
-        }
+
         Toolbar toolbar = findViewById(R.id.toolbar);
         setSupportActionBar(toolbar);
 
         setupActionBar();
+
+        swiper = findViewById(R.id.transaction_swipe);
+        if (swiper == null) throw new RuntimeException("Can't get hold on the swipe layout");
+        root = findViewById(R.id.transaction_root);
+        if (root == null) throw new RuntimeException("Can't get hold on the transaction list view");
+        progressBar = findViewById(R.id.transaction_progress_bar);
+        if (progressBar == null)
+            throw new RuntimeException("Can't get hold on the transaction list progress bar");
+        tvLastUpdate = findViewById(R.id.transactions_last_update);
+        {
+            long last_update = (new MobileLedgerDatabase(this))
+                    .get_option_value("transaction_list_last_update", 0);
+            if (last_update == 0) tvLastUpdate.setText("never");
+            else {
+                Date date = new Date(last_update);
+                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+                    tvLastUpdate.setText(date.toInstant().atZone(ZoneId.systemDefault())
+                            .format(DateTimeFormatter.ISO_LOCAL_DATE_TIME));
+                }
+                else {
+                    tvLastUpdate.setText(date.toLocaleString());
+                }
+            }
+        }
+        model = ViewModelProviders.of(this).get(TransactionListViewModel.class);
+        List<LedgerTransaction> transactions =
+                model.getTransactions(new MobileLedgerDatabase(this));
+        modelAdapter = new TransactionListAdapter(transactions);
+
+        RecyclerView root = findViewById(R.id.transaction_root);
+        root.setAdapter(modelAdapter);
+
+        LinearLayoutManager llm = new LinearLayoutManager(this);
+        llm.setOrientation(LinearLayoutManager.VERTICAL);
+        root.setLayoutManager(llm);
+
+        ((SwipeRefreshLayout) findViewById(R.id.transaction_swipe)).setOnRefreshListener(() -> {
+            Log.d("ui", "refreshing transactions via swipe");
+            update_transactions();
+        });
+
+//        update_transactions();
     }
     private void setupActionBar() {
         ActionBar actionBar = getSupportActionBar();
@@ -53,4 +115,55 @@ public class TransactionListActivity extends AppCompatActivity {
         Log.d("visuals", "finishing");
         overridePendingTransition(R.anim.dummy, R.anim.slide_out_right);
     }
+    private void update_transactions() {
+        RetrieveTransactionsTask task = new RetrieveTransactionsTask(new WeakReference<>(this));
+
+        RetrieveTransactionsTask.Params params = new RetrieveTransactionsTask.Params(
+                PreferenceManager.getDefaultSharedPreferences(this));
+
+        task.execute(params);
+    }
+
+    public void onRetrieveStart() {
+        progressBar.setIndeterminate(true);
+        progressBar.setVisibility(View.VISIBLE);
+    }
+    public void onRetrieveProgress(RetrieveTransactionsTask.Progress progress) {
+        if ((progress.getTotal() == RetrieveTransactionsTask.Progress.INDETERMINATE) ||
+            (progress.getTotal() == 0))
+        {
+            progressBar.setIndeterminate(true);
+        }
+        else {
+            progressBar.setIndeterminate(false);
+            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());
+        }
+    }
+
+    public void onRetrieveDone(boolean success) {
+        progressBar.setVisibility(View.GONE);
+        SwipeRefreshLayout srl = findViewById(R.id.transaction_swipe);
+        srl.setRefreshing(false);
+        if (success) {
+            MobileLedgerDatabase dbh = new MobileLedgerDatabase(this);
+            Date now = new Date();
+            dbh.set_option_value("transaction_list_last_update", now.getTime());
+            updateLastUpdateText(now);
+        }
+    }
+    private void updateLastUpdateText(Date now) {
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+            tvLastUpdate.setText(now.toInstant().atZone(ZoneId.systemDefault()).toString());
+        }
+        else {
+            tvLastUpdate.setText(now.toLocaleString());
+        }
+    }
 }
diff --git a/app/src/main/java/net/ktnx/mobileledger/TransactionListAdapter.java b/app/src/main/java/net/ktnx/mobileledger/TransactionListAdapter.java
new file mode 100644 (file)
index 0000000..f190efc
--- /dev/null
@@ -0,0 +1,89 @@
+/*
+ * Copyright © 2018 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;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.os.Build;
+import android.support.annotation.NonNull;
+import android.support.v7.widget.RecyclerView;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.LinearLayout;
+import android.widget.TableLayout;
+import android.widget.TextView;
+
+import net.ktnx.mobileledger.model.LedgerTransaction;
+
+import java.util.List;
+
+class TransactionListAdapter
+        extends RecyclerView.Adapter<TransactionListAdapter.TransactionRowHolder> {
+    private List<LedgerTransaction> transactions;
+
+    TransactionListAdapter(List<LedgerTransaction> transactions) {
+        this.transactions = transactions;
+    }
+
+    public void onBindViewHolder(@NonNull TransactionRowHolder holder, int position) {
+        LedgerTransaction tr = transactions.get(position);
+        Context ctx = holder.row.getContext();
+        Resources rm = ctx.getResources();
+
+        holder.tvDescription.setText(String.format("%s\n%s", tr.getDescription(), tr.getDate()));
+//        holder.tableAccounts.setText(acc.getAmountsString());
+
+        if (position % 2 == 0) {
+            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) holder.row
+                    .setBackgroundColor(rm.getColor(R.color.table_row_even_bg, ctx.getTheme()));
+            else holder.row.setBackgroundColor(rm.getColor(R.color.table_row_even_bg));
+        }
+        else {
+            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) holder.row
+                    .setBackgroundColor(rm.getColor(R.color.drawer_background, ctx.getTheme()));
+            else holder.row.setBackgroundColor(rm.getColor(R.color.drawer_background));
+        }
+
+        holder.row.setTag(R.id.POS, position);
+    }
+
+    @NonNull
+    @Override
+    public TransactionRowHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
+        View row = LayoutInflater.from(parent.getContext())
+                .inflate(R.layout.transaction_list_row, parent, false);
+        return new TransactionRowHolder(row);
+    }
+
+    @Override
+    public int getItemCount() {
+        return transactions.size();
+    }
+    class TransactionRowHolder extends RecyclerView.ViewHolder {
+        TextView tvDescription;
+        TableLayout tableAccounts;
+        LinearLayout row;
+        public TransactionRowHolder(@NonNull View itemView) {
+            super(itemView);
+            this.row = (LinearLayout) itemView;
+            this.tvDescription = itemView.findViewById(R.id.transaction_row_description);
+            this.tableAccounts = itemView.findViewById(R.id.transaction_row_acc_amounts);
+        }
+    }
+}
\ No newline at end of file
index aeb49a5f104b9ebcebda479b961fe83e855464db..2e3ca764d9897b1adecf718dd814d2e7b914ae8e 100644 (file)
 
 package net.ktnx.mobileledger.async;
 
+import android.annotation.SuppressLint;
 import android.content.Context;
 import android.content.SharedPreferences;
 import android.database.sqlite.SQLiteDatabase;
 import android.os.AsyncTask;
+import android.util.Log;
 
 import net.ktnx.mobileledger.R;
+import net.ktnx.mobileledger.TransactionListActivity;
 import net.ktnx.mobileledger.model.LedgerTransaction;
 import net.ktnx.mobileledger.model.LedgerTransactionItem;
 import net.ktnx.mobileledger.utils.MobileLedgerDatabase;
@@ -40,51 +43,55 @@ import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 
 
-class RetrieveTransactionsTask extends AsyncTask<RetrieveTransactionsTask.Params, Integer, Void> {
-    class Params {
-        static final int DEFAULT_LIMIT = 100;
-        private SharedPreferences backendPref;
-        private String accountsRoot;
-        private int limit;
-
-        Params(SharedPreferences backendPref) {
-            this.backendPref = backendPref;
-            this.accountsRoot = null;
-            this.limit = DEFAULT_LIMIT;
-        }
-        Params(SharedPreferences backendPref, String accountsRoot) {
-            this(backendPref, accountsRoot, DEFAULT_LIMIT);
-        }
-        Params(SharedPreferences backendPref, String accountsRoot, int limit) {
-            this.backendPref = backendPref;
-            this.accountsRoot = accountsRoot;
-            this.limit = limit;
-        }
-        String getAccountsRoot() {
-            return accountsRoot;
-        }
-        SharedPreferences getBackendPref() {
-            return backendPref;
-        }
-        int getLimit() {
-            return limit;
-        }
-    }
-    private static final Pattern transactionStartPattern = Pattern.compile("<tr class=\"title\" "
-            + "id=\"transaction-(\\d+)\"><td class=\"date\"[^\\\"]*>([\\d.-]+)</td>");
+public class RetrieveTransactionsTask extends
+        AsyncTask<RetrieveTransactionsTask.Params, RetrieveTransactionsTask.Progress, Void> {
+    private static final Pattern transactionStartPattern = Pattern.compile("<tr class=\"title\" " +
+                                                                           "id=\"transaction-(\\d+)\"><td class=\"date\"[^\\\"]*>([\\d.-]+)</td>");
     private static final Pattern transactionDescriptionPattern =
             Pattern.compile("<tr class=\"posting\" title=\"(\\S+)\\s(.+)");
     private static final Pattern transactionDetailsPattern =
             Pattern.compile("^\\s+" + "(\\S[\\S\\s]+\\S)\\s\\s+([-+]?\\d[\\d,.]*)");
-    protected WeakReference<Context> contextRef;
+    private static final Pattern endPattern = Pattern.compile("\\bid=\"addmodal\"");
+    protected WeakReference<TransactionListActivity> contextRef;
     protected int error;
+    private boolean success;
+    public RetrieveTransactionsTask(WeakReference<TransactionListActivity> contextRef) {
+        this.contextRef = contextRef;
+    }
+    private static final void L(String msg) {
+        Log.d("transaction-parser", msg);
+    }
+    @Override
+    protected void onProgressUpdate(Progress... values) {
+        super.onProgressUpdate(values);
+        TransactionListActivity context = getContext();
+        if (context == null) return;
+        context.onRetrieveProgress(values[0]);
+    }
+    @Override
+    protected void onPreExecute() {
+        super.onPreExecute();
+        TransactionListActivity context = getContext();
+        if (context == null) return;
+        context.onRetrieveStart();
+    }
+    @Override
+    protected void onPostExecute(Void aVoid) {
+        super.onPostExecute(aVoid);
+        TransactionListActivity context = getContext();
+        if (context == null) return;
+        context.onRetrieveDone(success);
+    }
+    @SuppressLint("DefaultLocale")
     @Override
     protected Void doInBackground(Params... params) {
+        Progress progress = new Progress();
+        success = false;
         try {
             HttpURLConnection http =
                     NetworkUtil.prepare_connection(params[0].getBackendPref(), "journal");
             http.setAllowUserInteraction(false);
-            publishProgress(0);
+            publishProgress(progress);
             Context ctx = contextRef.get();
             if (ctx == null) return null;
             try (MobileLedgerDatabase dbh = new MobileLedgerDatabase(ctx)) {
@@ -95,18 +102,7 @@ class RetrieveTransactionsTask extends AsyncTask<RetrieveTransactionsTask.Params
                         db.beginTransaction();
                         try {
                             String root = params[0].getAccountsRoot();
-                            if (root == null) db.execSQL("DELETE FROM transaction_history;");
-                            else {
-                                StringBuilder sql = new StringBuilder();
-                                sql.append("DELETE FROM transaction_history ");
-                                sql.append(
-                                        "where id in (select transactions.id from transactions ");
-                                sql.append("join transaction_accounts ");
-                                sql.append(
-                                        "on transactions.id=transaction_accounts.transaction_id ");
-                                sql.append("where transaction_accounts.account_name like ?||'%'");
-                                db.execSQL(sql.toString(), new String[]{root});
-                            }
+                            db.execSQL("DELETE FROM transactions;");
 
                             int state = ParserState.EXPECTING_JOURNAL;
                             String line;
@@ -114,48 +110,66 @@ class RetrieveTransactionsTask extends AsyncTask<RetrieveTransactionsTask.Params
                                     new BufferedReader(new InputStreamReader(resp, "UTF-8"));
 
                             int transactionCount = 0;
-                            String transactionId = null;
+                            int transactionId = 0;
                             LedgerTransaction transaction = null;
+                            LINES:
                             while ((line = buf.readLine()) != null) {
+                                Matcher m;
+                                L(String.format("State is %d", state));
                                 switch (state) {
-                                    case ParserState.EXPECTING_JOURNAL: {
-                                        if (line.equals("<h2>General Journal</h2>"))
+                                    case ParserState.EXPECTING_JOURNAL:
+                                        if (line.equals("<h2>General Journal</h2>")) {
                                             state = ParserState.EXPECTING_TRANSACTION;
-                                        continue;
-                                    }
-                                    case ParserState.EXPECTING_TRANSACTION: {
-                                        Matcher m = transactionStartPattern.matcher(line);
+                                            L("→ expecting transaction");
+                                        }
+                                        break;
+                                    case ParserState.EXPECTING_TRANSACTION:
+                                        m = transactionStartPattern.matcher(line);
                                         if (m.find()) {
-                                            transactionId = m.group(1);
+                                            transactionId = Integer.valueOf(m.group(1));
                                             state = ParserState.EXPECTING_TRANSACTION_DESCRIPTION;
+                                            L(String.format("found transaction %d → expecting " +
+                                                            "description", transactionId));
+                                            progress.setProgress(++transactionCount);
+                                            if (progress.getTotal() == Progress.INDETERMINATE)
+                                                progress.setTotal(transactionId);
+                                            publishProgress(progress);
                                         }
-                                    }
-                                    case ParserState.EXPECTING_TRANSACTION_DESCRIPTION: {
-                                        Matcher m = transactionDescriptionPattern.matcher(line);
+                                        m = endPattern.matcher(line);
                                         if (m.find()) {
-                                            if (transactionId == null)
+                                            L("--- transaction list complete ---");
+                                            success = true;
+                                            break LINES;
+                                        }
+                                        break;
+                                    case ParserState.EXPECTING_TRANSACTION_DESCRIPTION:
+                                        m = transactionDescriptionPattern.matcher(line);
+                                        if (m.find()) {
+                                            if (transactionId == 0)
                                                 throw new TransactionParserException(
-                                                        "Transaction Id is null while expecting description");
+                                                        "Transaction Id is 0 while expecting " +
+                                                        "description");
 
                                             transaction =
                                                     new LedgerTransaction(transactionId, m.group(1),
                                                             m.group(2));
                                             state = ParserState.EXPECTING_TRANSACTION_DETAILS;
+                                            L(String.format("transaction %d created for %s (%s) →" +
+                                                            " expecting details", transactionId,
+                                                    m.group(1), m.group(2)));
                                         }
-                                    }
-                                    case ParserState.EXPECTING_TRANSACTION_DETAILS: {
-                                        if (transaction == null)
-                                            throw new TransactionParserException(
-                                                    "Transaction is null while expecting details");
+                                        break;
+                                    case ParserState.EXPECTING_TRANSACTION_DETAILS:
                                         if (line.isEmpty()) {
                                             // transaction data collected
                                             transaction.insertInto(db);
 
                                             state = ParserState.EXPECTING_TRANSACTION;
-                                            publishProgress(++transactionCount);
+                                            L(String.format("transaction %s saved → expecting " +
+                                                            "transaction", transaction.getId()));
                                         }
                                         else {
-                                            Matcher m = transactionDetailsPattern.matcher(line);
+                                            m = transactionDetailsPattern.matcher(line);
                                             if (m.find()) {
                                                 String acc_name = m.group(1);
                                                 String amount = m.group(2);
@@ -163,11 +177,12 @@ class RetrieveTransactionsTask extends AsyncTask<RetrieveTransactionsTask.Params
                                                 transaction.add_item(
                                                         new LedgerTransactionItem(acc_name,
                                                                 Float.valueOf(amount)));
+                                                L(String.format("%s = %s", acc_name, amount));
                                             }
                                             else throw new IllegalStateException(String.format(
-                                                    "Can't" + " parse transaction details"));
+                                                    "Can't parse transaction details"));
                                         }
-                                    }
+                                        break;
                                     default:
                                         throw new RuntimeException(
                                                 String.format("Unknown " + "parser state %d",
@@ -197,8 +212,63 @@ class RetrieveTransactionsTask extends AsyncTask<RetrieveTransactionsTask.Params
         }
         return null;
     }
-    WeakReference<Context> getContextRef() {
-        return contextRef;
+    TransactionListActivity getContext() {
+        return contextRef.get();
+    }
+
+    public static class Params {
+        static final int DEFAULT_LIMIT = 100;
+        private SharedPreferences backendPref;
+        private String accountsRoot;
+        private int limit;
+
+        public Params(SharedPreferences backendPref) {
+            this.backendPref = backendPref;
+            this.accountsRoot = null;
+            this.limit = DEFAULT_LIMIT;
+        }
+        Params(SharedPreferences backendPref, String accountsRoot) {
+            this(backendPref, accountsRoot, DEFAULT_LIMIT);
+        }
+        Params(SharedPreferences backendPref, String accountsRoot, int limit) {
+            this.backendPref = backendPref;
+            this.accountsRoot = accountsRoot;
+            this.limit = limit;
+        }
+        String getAccountsRoot() {
+            return accountsRoot;
+        }
+        SharedPreferences getBackendPref() {
+            return backendPref;
+        }
+        int getLimit() {
+            return limit;
+        }
+    }
+
+    public class Progress {
+        public static final int INDETERMINATE = -1;
+        private int progress;
+        private int total;
+        Progress() {
+            this(INDETERMINATE, INDETERMINATE);
+        }
+        Progress(int progress, int total) {
+            this.progress = progress;
+            this.total = total;
+        }
+        public int getProgress() {
+            return progress;
+        }
+        protected void setProgress(int progress) {
+            this.progress = progress;
+        }
+        public int getTotal() {
+            return total;
+        }
+        protected void setTotal(int total) {
+            this.total = total;
+        }
     }
 
     private class TransactionParserException extends IllegalStateException {
index a103ab3951d92c0ef643f81dbaf15f45c52c1a1c..8aa169dc05a6893a8d3d2f54be37eef5daa30d89 100644 (file)
@@ -19,45 +19,63 @@ package net.ktnx.mobileledger.model;
 
 import android.database.sqlite.SQLiteDatabase;
 
+import net.ktnx.mobileledger.utils.Digest;
+
+import java.nio.charset.Charset;
+import java.security.NoSuchAlgorithmException;
 import java.util.ArrayList;
+import java.util.Comparator;
 import java.util.Iterator;
-import java.util.List;
 
 public class LedgerTransaction {
+    private static final String DIGEST_TYPE = "SHA-256";
+    public final Comparator<LedgerTransactionItem> comparator =
+            new Comparator<LedgerTransactionItem>() {
+                @Override
+                public int compare(LedgerTransactionItem o1, LedgerTransactionItem o2) {
+                    int res = o1.getAccountName().compareTo(o2.getAccountName());
+                    if (res != 0) return res;
+                    res = o1.getCurrency().compareTo(o2.getCurrency());
+                    if (res != 0) return res;
+                    return Float.compare(o1.getAmount(), o2.getAmount());
+                }
+            };
     private String id;
     private String date;
     private String description;
-    private List<LedgerTransactionItem> items;
-
+    private ArrayList<LedgerTransactionItem> items;
+    private String dataHash;
     public LedgerTransaction(String id, String date, String description) {
         this.id = id;
         this.date = date;
         this.description = description;
         this.items = new ArrayList<>();
+        this.dataHash = null;
+    }
+    public LedgerTransaction(int id, String date, String description) {
+        this(String.valueOf(id), date, description);
     }
     public LedgerTransaction(String date, String description) {
         this(null, date, description);
     }
     public void add_item(LedgerTransactionItem item) {
         items.add(item);
+        dataHash = null;
     }
-
     public String getDate() {
         return date;
     }
-
     public void setDate(String date) {
         this.date = date;
+        dataHash = null;
     }
-
     public String getDescription() {
         return description;
     }
-
     public void setDescription(String description) {
         this.description = description;
+        dataHash = null;
     }
-
     public Iterator<LedgerTransactionItem> getItemsIterator() {
         return new Iterator<LedgerTransactionItem>() {
             private int pointer = 0;
@@ -75,15 +93,39 @@ public class LedgerTransaction {
     public String getId() {
         return id;
     }
-
     public void insertInto(SQLiteDatabase db) {
-        db.execSQL("INSERT INTO transactions(id, date, " + "description) values(?, ?, ?)",
+        fillDataHash();
+        db.execSQL("INSERT INTO transactions(id, date, description, data_hash) values(?,?,?,?)",
                 new String[]{id, date, description});
 
-        for(LedgerTransactionItem item : items) {
-            db.execSQL("INSERT INTO transaction_accounts(transaction_id, account_name, amount, "
-                    + "currency) values(?, ?, ?, ?)", new Object[]{id, item.getAccountName(),
-                                                                   item.getAmount(), item.getCurrency()});
+        for (LedgerTransactionItem item : items) {
+            db.execSQL("INSERT INTO transaction_accounts(transaction_id, account_name, amount, " +
+                       "currency) values(?, ?, ?, ?)",
+                    new Object[]{id, item.getAccountName(), item.getAmount(), item.getCurrency()});
+        }
+    }
+    private void fillDataHash() {
+        if (dataHash != null) return;
+        try {
+            Digest sha = new Digest(DIGEST_TYPE);
+            StringBuilder data = new StringBuilder();
+            data.append(getId());
+            data.append('\0');
+            data.append(getDescription());
+            data.append('\0');
+            for (LedgerTransactionItem item : items) {
+                data.append(item.getAccountName());
+                data.append('\0');
+                data.append(item.getCurrency());
+                data.append('\0');
+                data.append(item.getAmount());
+            }
+            sha.update(data.toString().getBytes(Charset.forName("UTF-8")));
+            dataHash = sha.digestToHexString();
+        }
+        catch (NoSuchAlgorithmException e) {
+            throw new RuntimeException(
+                    String.format("Unable to get instance of %s digest", DIGEST_TYPE), e);
         }
     }
 }
index 8cd758f78eaf14581bbc2023bceed37fcac08ffc..97d2fe80bdd817657c02242d481bfd4ed86ee9cf 100644 (file)
@@ -22,6 +22,7 @@ import android.os.Bundle;
 import android.support.annotation.NonNull;
 import android.support.annotation.Nullable;
 import android.support.v4.app.Fragment;
+import android.util.Log;
 import android.view.LayoutInflater;
 import android.view.View;
 import android.view.ViewGroup;
@@ -43,6 +44,7 @@ public class TransactionListFragment extends Fragment {
 
     @Override
     public void onActivityCreated(@Nullable Bundle savedInstanceState) {
+        Log.d("flow", "TransactionListFragment.onActivityCreated called");
         super.onActivityCreated(savedInstanceState);
         mViewModel = ViewModelProviders.of(this).get(TransactionListViewModel.class);
         // TODO: Use the ViewModel
index 10aa2e9bc62630949cf9df03607463bc12d306de..35b011458c8f36759d3e520a2d1552c98fb8a946 100644 (file)
 package net.ktnx.mobileledger.ui.transaction_list;
 
 import android.arch.lifecycle.ViewModel;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+
+import net.ktnx.mobileledger.model.LedgerTransaction;
+import net.ktnx.mobileledger.utils.MobileLedgerDatabase;
+
+import java.util.ArrayList;
+import java.util.List;
 
 public class TransactionListViewModel extends ViewModel {
-    // TODO: Implement the ViewModel
+
+    private List<LedgerTransaction> transactions;
+
+    public List<LedgerTransaction> getTransactions(MobileLedgerDatabase dbh) {
+        if (transactions == null) {
+            transactions = new ArrayList<>();
+            reloadTransactions(dbh);
+        }
+
+        return transactions;
+    }
+    private void reloadTransactions(MobileLedgerDatabase dbh) {
+        transactions.clear();
+        String sql = "SELECT id, date, description FROM transactions";
+        sql += " ORDER BY date desc, id desc";
+
+        try (SQLiteDatabase db = dbh.getReadableDatabase()) {
+            try (Cursor cursor = db.rawQuery(sql, null)) {
+                while (cursor.moveToNext()) {
+                    LedgerTransaction tr =
+                            new LedgerTransaction(cursor.getString(0), cursor.getString(1),
+                                    cursor.getString(2));
+                    // TODO: fill accounts and amounts
+                    transactions.add(tr);
+                }
+            }
+        }
+
+    }
 }
diff --git a/app/src/main/java/net/ktnx/mobileledger/utils/Digest.java b/app/src/main/java/net/ktnx/mobileledger/utils/Digest.java
new file mode 100644 (file)
index 0000000..c25ca59
--- /dev/null
@@ -0,0 +1,89 @@
+/*
+ * Copyright © 2018 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.utils;
+
+import java.nio.ByteBuffer;
+import java.security.DigestException;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+
+public class Digest {
+    private MessageDigest digest;
+    public Digest(String type) throws NoSuchAlgorithmException {
+        digest = MessageDigest.getInstance(type);
+    }
+    public static char[] hexDigitsFor(byte x) {
+        return hexDigitsFor((int) ((x<0) ? 256+x : x));
+    }
+    public static char[] hexDigitsFor(int x) {
+        if ((x < 0) || (x > 255)) throw new ArithmeticException(
+                String.format("Hex digits must be between 0 and 255 (argument: %d)", x));
+        char[] result = new char[]{0, 0};
+        result[0] = hexDigitFor(x / 16);
+        result[1] = hexDigitFor(x % 16);
+
+        return result;
+    }
+    public static char hexDigitFor(int x) {
+        if (x < 0) throw new ArithmeticException(
+                String.format("Hex digits can't be negative (argument: %d)", x));
+        if (x < 10) return (char) ('0' + x);
+        if (x < 16) return (char) ('a' + x - 10);
+        throw new ArithmeticException(
+                String.format("Hex digits can't be greater than 15 (argument: %d)", x));
+    }
+    public void update(byte input) {
+        digest.update(input);
+    }
+    public void update(byte[] input, int offset, int len) {
+        digest.update(input, offset, len);
+    }
+    public void update(byte[] input) {
+        digest.update(input);
+    }
+    public void update(ByteBuffer input) {
+        digest.update(input);
+    }
+    public byte[] digest() {
+        return digest.digest();
+    }
+    public int digest(byte[] buf, int offset, int len) throws DigestException {
+        return digest.digest(buf, offset, len);
+    }
+    public byte[] digest(byte[] input) {
+        return digest.digest(input);
+    }
+    public String digestToHexString() {
+        byte[] digest = digest();
+        StringBuilder result = new StringBuilder();
+        for (int i = 0; i < getDigestLength(); i++) {
+            result.append(hexDigitsFor(digest[i]));
+        }
+        return result.toString();
+    }
+    public void reset() {
+        digest.reset();
+    }
+    public int getDigestLength() {
+        return digest.getDigestLength();
+    }
+    @Override
+    public Object clone() throws CloneNotSupportedException {
+        return digest.clone();
+    }
+}
index cae65b0f2a517ebfb38f75fc91ec9f76644c54bb..e73c45a60bf0c69a1a8ffd73b29b8fd5bacb53bd 100644 (file)
@@ -35,7 +35,7 @@ public class MobileLedgerDatabase extends SQLiteOpenHelper implements AutoClosea
     public static final String DB_NAME = "mobile-ledger.db";
     public static final String ACCOUNTS_TABLE = "accounts";
     public static final String DESCRIPTION_HISTORY_TABLE = "description_history";
-    public static final int LATEST_REVISION = 7;
+    public static final int LATEST_REVISION = 8;
 
     private final Context mContext;
 
index 1fd9399b83046e0039c44977dd4b3c60472734bd..afb1d2efc745adb89a33f3f87501e0caf7d5d613 100644 (file)
@@ -15,8 +15,7 @@
   ~ along with Mobile-Ledger. If not, see <https://www.gnu.org/licenses/>.
   -->
 
-<android.support.design.widget.CoordinatorLayout
-    xmlns:android="http://schemas.android.com/apk/res/android"
+<android.support.design.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:app="http://schemas.android.com/apk/res-auto"
     xmlns:tools="http://schemas.android.com/tools"
     android:layout_width="match_parent"
 
     </android.support.design.widget.AppBarLayout>
 
-    <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
-        xmlns:tools="http://schemas.android.com/tools"
-        android:id="@+id/container"
-        android:layout_width="match_parent"
-        android:layout_height="match_parent"
-        tools:context=".TransactionListActivity" />
+    <include layout="@layout/transaction_list_fragment" />
 </android.support.design.widget.CoordinatorLayout>
index e76ff29b4ea6e75e0bf1b3784ff995b4545cc962..6906ac1aed9dc52069f5425dfda3d33ffe8f587b 100644 (file)
@@ -1,5 +1,4 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
+<?xml version="1.0" encoding="utf-8"?><!--
   ~ Copyright © 2018 Damyan Ivanov.
   ~ This file is part of Mobile-Ledger.
   ~ Mobile-Ledger is free software: you can distribute it and/or modify it
     xmlns:tools="http://schemas.android.com/tools"
     android:id="@+id/transaction_list"
     android:layout_width="match_parent"
-    android:layout_height="match_parent"
-    tools:context=".ui.transaction_list.TransactionListFragment">
+    android:layout_height="wrap_content"
+    app:layout_behavior="@string/appbar_scrolling_view_behavior"
+    tools:context=".TransactionListActivity">
 
-    <TextView
-        android:id="@+id/message"
-        android:layout_width="wrap_content"
+    <LinearLayout
+        android:id="@+id/last_update_row"
+        android:layout_width="0dp"
         android:layout_height="wrap_content"
-        android:text="TransactionListFragment"
-        app:layout_constraintBottom_toBottomOf="parent"
+        android:orientation="horizontal"
         app:layout_constraintEnd_toEndOf="parent"
         app:layout_constraintStart_toStartOf="parent"
-        app:layout_constraintTop_toTopOf="parent" />
+        app:layout_constraintTop_toTopOf="parent">
 
+        <TextView
+            android:id="@+id/transaction_last_update_label"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:paddingEnd="8dp"
+            android:text="@string/transactions_last_update_label"
+            app:layout_constraintEnd_toEndOf="parent"
+            app:layout_constraintStart_toStartOf="parent" />
+
+        <TextView
+            android:id="@+id/transactions_last_update"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_weight="1"
+            android:text="TextView" />
+    </LinearLayout>
+
+    <ProgressBar
+        android:id="@+id/transaction_progress_bar"
+        style="?android:attr/progressBarStyleHorizontal"
+        android:layout_width="0dp"
+        android:layout_height="wrap_content"
+        android:indeterminate="true"
+        android:indeterminateBehavior="cycle"
+        android:progress="40"
+        android:visibility="gone"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toBottomOf="@+id/last_update_row" />
+
+    <android.support.v4.widget.SwipeRefreshLayout
+        android:id="@+id/transaction_swipe"
+        android:layout_width="match_parent"
+        android:layout_height="0dp"
+        app:layout_constraintTop_toBottomOf="@+id/transaction_progress_bar">
+
+        <android.support.v7.widget.RecyclerView
+            android:id="@+id/transaction_root"
+            android:layout_width="match_parent"
+            android:layout_height="match_parent" />
+    </android.support.v4.widget.SwipeRefreshLayout>
 </android.support.constraint.ConstraintLayout>
diff --git a/app/src/main/res/layout/transaction_list_row.xml b/app/src/main/res/layout/transaction_list_row.xml
new file mode 100644 (file)
index 0000000..8a950ef
--- /dev/null
@@ -0,0 +1,72 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+
+<!--
+  ~ Copyright © 2018 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/>.
+  -->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:id="@+id/transaction_row"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:gravity="center_vertical"
+    android:minHeight="36dp"
+    android:orientation="horizontal"
+    android:paddingStart="8dp"
+    android:paddingEnd="8dp"
+    tools:showIn="@layout/transaction_list_fragment">
+
+    <!--android:button="@drawable/checkbox_star_black"-->
+
+    <TextView
+        android:id="@+id/transaction_row_description"
+        style="@style/account_summary_account_name"
+        android:text="Sample description goes here."
+        tools:ignore="HardcodedText" />
+
+    <TableLayout
+        android:id="@+id/transaction_row_acc_amounts"
+        android:layout_width="wrap_content"
+        android:layout_height="match_parent">
+
+        <TableRow>
+
+            <TextView
+                style="@style/account_summary_amounts"
+                android:text="Sample account name"
+                tools:ignore="HardcodedText" />
+
+            <TextView
+                style="@style/account_summary_amounts"
+                android:text="123,45\n678,90"
+                tools:ignore="HardcodedText" />
+        </TableRow>
+
+        <TableRow>
+
+            <TextView
+                style="@style/account_summary_amounts"
+                android:text="Sample account name"
+                tools:ignore="HardcodedText" />
+
+            <TextView
+                style="@style/account_summary_amounts"
+                android:text="123,45\n678,90"
+                tools:ignore="HardcodedText" />
+        </TableRow>
+    </TableLayout>
+</LinearLayout>
\ No newline at end of file
diff --git a/app/src/main/res/raw/sql_8.sql b/app/src/main/res/raw/sql_8.sql
new file mode 100644 (file)
index 0000000..0f06539
--- /dev/null
@@ -0,0 +1,2 @@
+alter table transactions add data_hash varchar;
+delete from transactions;
\ No newline at end of file
index c6d1d8a66ccef21a35a347767ded8b31b67890f7..da63a4cb6c1227b8ec99625fa70331797c239a02 100644 (file)
     <string name="menu_acc_summary_cancel_selection_title">Cancel selection</string>
     <string name="menu_acc_summary_confirm_selection_title">Confirm selectin</string>
     <string name="title_activity_transaction_list">Transactions</string>
+    <string name="transactions_last_update_label">Last update:</string>
 </resources>