]> git.ktnx.net Git - mobile-ledger.git/commitdiff
separate packages for the different aspects of the application
authorDamyan Ivanov <dam+mobileledger@ktnx.net>
Fri, 14 Dec 2018 20:56:35 +0000 (20:56 +0000)
committerDamyan Ivanov <dam+mobileledger@ktnx.net>
Fri, 14 Dec 2018 20:56:35 +0000 (20:56 +0000)
29 files changed:
app/src/main/java/net/ktnx/mobileledger/AccountSummary.java
app/src/main/java/net/ktnx/mobileledger/AccountSummaryViewModel.java
app/src/main/java/net/ktnx/mobileledger/DatePickerFragment.java [deleted file]
app/src/main/java/net/ktnx/mobileledger/DimensionUtils.java [deleted file]
app/src/main/java/net/ktnx/mobileledger/LedgerAccount.java [deleted file]
app/src/main/java/net/ktnx/mobileledger/LedgerAmount.java [deleted file]
app/src/main/java/net/ktnx/mobileledger/LedgerTransaction.java [deleted file]
app/src/main/java/net/ktnx/mobileledger/LedgerTransactionItem.java [deleted file]
app/src/main/java/net/ktnx/mobileledger/MobileLedgerDatabase.java [deleted file]
app/src/main/java/net/ktnx/mobileledger/NetworkUtil.java [deleted file]
app/src/main/java/net/ktnx/mobileledger/NewTransactionActivity.java
app/src/main/java/net/ktnx/mobileledger/RetrieveAccountsTask.java [deleted file]
app/src/main/java/net/ktnx/mobileledger/RetrieveTransactionsTask.java [deleted file]
app/src/main/java/net/ktnx/mobileledger/SaveTransactionTask.java [deleted file]
app/src/main/java/net/ktnx/mobileledger/TaskCallback.java [deleted file]
app/src/main/java/net/ktnx/mobileledger/UrlEncodedFormData.java [deleted file]
app/src/main/java/net/ktnx/mobileledger/async/RetrieveAccountsTask.java [new file with mode: 0644]
app/src/main/java/net/ktnx/mobileledger/async/RetrieveTransactionsTask.java [new file with mode: 0644]
app/src/main/java/net/ktnx/mobileledger/async/SaveTransactionTask.java [new file with mode: 0644]
app/src/main/java/net/ktnx/mobileledger/async/TaskCallback.java [new file with mode: 0644]
app/src/main/java/net/ktnx/mobileledger/model/LedgerAccount.java [new file with mode: 0644]
app/src/main/java/net/ktnx/mobileledger/model/LedgerAmount.java [new file with mode: 0644]
app/src/main/java/net/ktnx/mobileledger/model/LedgerTransaction.java [new file with mode: 0644]
app/src/main/java/net/ktnx/mobileledger/model/LedgerTransactionItem.java [new file with mode: 0644]
app/src/main/java/net/ktnx/mobileledger/ui/DatePickerFragment.java [new file with mode: 0644]
app/src/main/java/net/ktnx/mobileledger/utils/DimensionUtils.java [new file with mode: 0644]
app/src/main/java/net/ktnx/mobileledger/utils/MobileLedgerDatabase.java [new file with mode: 0644]
app/src/main/java/net/ktnx/mobileledger/utils/NetworkUtil.java [new file with mode: 0644]
app/src/main/java/net/ktnx/mobileledger/utils/UrlEncodedFormData.java [new file with mode: 0644]

index b6b2b3b7a7c4ac6308f101385977a7d833944cdb..9182949828910225deee0e627cea72ae4e8457ae 100644 (file)
@@ -41,6 +41,10 @@ import android.view.MenuItem;
 import android.view.View;
 import android.widget.LinearLayout;
 
+import net.ktnx.mobileledger.async.RetrieveAccountsTask;
+import net.ktnx.mobileledger.model.LedgerAccount;
+import net.ktnx.mobileledger.utils.MobileLedgerDatabase;
+
 import java.lang.ref.WeakReference;
 import java.util.Date;
 import java.util.List;
@@ -259,7 +263,7 @@ public class AccountSummary extends AppCompatActivity {
         task.execute();
 
     }
-    void onAccountRefreshDone(int error) {
+    public void onAccountRefreshDone(int error) {
         SwipeRefreshLayout srl = findViewById(R.id.account_swiper);
         srl.setRefreshing(false);
         if (error != 0) {
index 0a523c7fa94266c02245186a85d2aa9c380dfea2..f01907b397d24c35933110bbcbe7fefc8ec971e2 100644 (file)
@@ -36,6 +36,9 @@ import android.widget.CheckBox;
 import android.widget.LinearLayout;
 import android.widget.TextView;
 
+import net.ktnx.mobileledger.model.LedgerAccount;
+import net.ktnx.mobileledger.utils.MobileLedgerDatabase;
+
 import java.util.ArrayList;
 import java.util.List;
 
diff --git a/app/src/main/java/net/ktnx/mobileledger/DatePickerFragment.java b/app/src/main/java/net/ktnx/mobileledger/DatePickerFragment.java
deleted file mode 100644 (file)
index ebee058..0000000
+++ /dev/null
@@ -1,111 +0,0 @@
-/*
- * 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.annotation.TargetApi;
-import android.app.DatePickerDialog;
-import android.app.Dialog;
-import android.os.Build;
-import android.os.Bundle;
-import android.support.annotation.NonNull;
-import android.support.v7.app.AppCompatDialogFragment;
-import android.widget.DatePicker;
-import android.widget.TextView;
-
-import java.util.Calendar;
-import java.util.GregorianCalendar;
-import java.util.Locale;
-import java.util.Objects;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
-
-public class DatePickerFragment extends AppCompatDialogFragment
-implements DatePickerDialog.OnDateSetListener, DatePicker.OnDateChangedListener
-{
-    @NonNull
-    @Override
-    public Dialog onCreateDialog(Bundle savedInstanceState) {
-        final Calendar c = GregorianCalendar.getInstance();
-        int year = c.get(GregorianCalendar.YEAR);
-        int month = c.get(GregorianCalendar.MONTH);
-        int day = c.get(GregorianCalendar.DAY_OF_MONTH);
-        TextView date = Objects.requireNonNull(getActivity()).findViewById(R.id.new_transaction_date);
-
-        CharSequence present = date.getText();
-
-        Pattern re_mon_day = Pattern.compile("^\\s*(\\d+)\\s*/\\s*(\\d+)\\s*$");
-        Matcher m_mon_day = re_mon_day.matcher(present);
-
-        if (m_mon_day.matches()) {
-            month = Integer.parseInt(m_mon_day.group(1))-1;
-            day = Integer.parseInt(m_mon_day.group(2));
-        }
-        else {
-            Pattern re_day = Pattern.compile("^\\s*(\\d{1,2})\\s*$");
-            Matcher m_day = re_day.matcher(present);
-            if (m_day.matches()) {
-                day = Integer.parseInt(m_day.group(1));
-            }
-        }
-
-        DatePickerDialog dpd =  new DatePickerDialog(Objects.requireNonNull(getActivity()), this, year, month, day);
-        // quicker date selection available in API 26
-        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
-            DatePicker dp = dpd.getDatePicker();
-            dp.setOnDateChangedListener(this);
-        }
-
-        return dpd;
-    }
-
-    @TargetApi(Build.VERSION_CODES.O)
-    public void onDateSet(DatePicker view, int year, int month, int day) {
-        TextView date = Objects.requireNonNull(getActivity()).findViewById(R.id.new_transaction_date);
-
-        final Calendar c = GregorianCalendar.getInstance();
-        if ( c.get(GregorianCalendar.YEAR) == year && c.get(GregorianCalendar.MONTH) == month) {
-            date.setText(String.format(Locale.US, "%d", day));
-        }
-        else {
-            date.setText(String.format(Locale.US, "%d/%d", month+1, day));
-        }
-
-        TextView description = Objects.requireNonNull(getActivity())
-                .findViewById(R.id.new_transaction_description);
-        description.requestFocus();
-    }
-
-    @Override
-    public void onDateChanged(DatePicker view, int year, int monthOfYear, int dayOfMonth) {
-        TextView date = Objects.requireNonNull(getActivity()).findViewById(R.id.new_transaction_date);
-
-        final Calendar c = GregorianCalendar.getInstance();
-        if ( c.get(GregorianCalendar.YEAR) == year && c.get(GregorianCalendar.MONTH) == monthOfYear) {
-            date.setText(String.format(Locale.US, "%d", dayOfMonth));
-        }
-        else {
-            date.setText(String.format(Locale.US, "%d/%d", monthOfYear+1, dayOfMonth));
-        }
-
-        TextView description = Objects.requireNonNull(getActivity())
-                .findViewById(R.id.new_transaction_description);
-        description.requestFocus();
-
-        this.dismiss();
-    }
-}
diff --git a/app/src/main/java/net/ktnx/mobileledger/DimensionUtils.java b/app/src/main/java/net/ktnx/mobileledger/DimensionUtils.java
deleted file mode 100644 (file)
index 836d7b7..0000000
+++ /dev/null
@@ -1,29 +0,0 @@
-/*
- * 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.util.TypedValue;
-
-public class DimensionUtils {
-    public static int dp2px(Context context, float dp) {
-        return Math.round(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp,
-               context.getResources().getDisplayMetrics()));
-    }
-
-}
diff --git a/app/src/main/java/net/ktnx/mobileledger/LedgerAccount.java b/app/src/main/java/net/ktnx/mobileledger/LedgerAccount.java
deleted file mode 100644 (file)
index 0c0c3ac..0000000
+++ /dev/null
@@ -1,130 +0,0 @@
-/*
- * 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.support.annotation.NonNull;
-
-import java.util.ArrayList;
-import java.util.List;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
-
-class LedgerAccount {
-    private String name;
-    private String shortName;
-    private int level;
-    private String parentName;
-    private boolean hidden;
-    private boolean hiddenToBe;
-    private List<LedgerAmount> amounts;
-    static Pattern higher_account = Pattern.compile("^[^:]+:");
-
-    LedgerAccount(String name) {
-        this.setName(name);
-        hidden = false;
-    }
-
-    public boolean isHidden() {
-        return hidden;
-    }
-
-    public void setHidden(boolean hidden) {
-        this.hidden = hidden;
-    }
-
-    LedgerAccount(String name, float amount) {
-        this.setName(name);
-        this.hidden = false;
-        this.amounts = new ArrayList<LedgerAmount>();
-        this.addAmount(amount);
-    }
-
-    public void setName(String name) {
-        this.name = name;
-        stripName();
-    }
-
-    private void stripName() {
-        level = 0;
-        shortName = name;
-        StringBuilder parentBuilder = new StringBuilder();
-        while (true) {
-            Matcher m = higher_account.matcher(shortName);
-            if (m.find()) {
-                level++;
-                parentBuilder.append(m.group(0));
-                shortName = m.replaceFirst("");
-            }
-            else break;
-        }
-        if (parentBuilder.length() > 0)
-            parentName = parentBuilder.substring(0, parentBuilder.length() - 1);
-        else parentName = null;
-    }
-
-    String getName() {
-        return name;
-    }
-
-    void addAmount(float amount, String currency) {
-        if (amounts == null ) amounts = new ArrayList<>();
-        amounts.add(new LedgerAmount(amount, currency));
-    }
-    void addAmount(float amount) {
-        this.addAmount(amount, null);
-    }
-
-    String getAmountsString() {
-        if ((amounts == null) || amounts.isEmpty()) return "";
-
-        StringBuilder builder = new StringBuilder();
-        for( LedgerAmount amount : amounts ) {
-            String amt = amount.toString();
-            if (builder.length() > 0) builder.append('\n');
-            builder.append(amt);
-        }
-
-        return builder.toString();
-    }
-
-    public int getLevel() {
-        return level;
-    }
-
-    @NonNull
-    public String getShortName() {
-        return shortName;
-    }
-
-    public String getParentName() {
-        return parentName;
-    }
-    public void togglehidden() {
-        hidden = !hidden;
-    }
-
-    public boolean isHiddenToBe() {
-        return hiddenToBe;
-    }
-    public void setHiddenToBe(boolean hiddenToBe) {
-        this.hiddenToBe = hiddenToBe;
-    }
-    public void toggleHiddenToBe() {
-        setHiddenToBe(!hiddenToBe);
-    }
-}
diff --git a/app/src/main/java/net/ktnx/mobileledger/LedgerAmount.java b/app/src/main/java/net/ktnx/mobileledger/LedgerAmount.java
deleted file mode 100644 (file)
index a59b891..0000000
+++ /dev/null
@@ -1,45 +0,0 @@
-/*
- * 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.annotation.SuppressLint;
-import android.support.annotation.NonNull;
-
-class LedgerAmount {
-    private String currency;
-    private float amount;
-
-    public
-    LedgerAmount(float amount, @NonNull String currency) {
-        this.currency = currency;
-        this.amount = amount;
-    }
-
-    public
-    LedgerAmount(float amount) {
-        this.amount = amount;
-        this.currency = null;
-    }
-
-    @SuppressLint("DefaultLocale")
-    @NonNull
-    public String toString() {
-        if (currency == null) return String.format("%,1.2f", amount);
-        else return String.format("%s %,1.2f", currency, amount);
-    }
-}
diff --git a/app/src/main/java/net/ktnx/mobileledger/LedgerTransaction.java b/app/src/main/java/net/ktnx/mobileledger/LedgerTransaction.java
deleted file mode 100644 (file)
index ad456ce..0000000
+++ /dev/null
@@ -1,89 +0,0 @@
-/*
- * 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.database.sqlite.SQLiteDatabase;
-
-import java.util.ArrayList;
-import java.util.Iterator;
-import java.util.List;
-
-class LedgerTransaction {
-    private String id;
-    private String date;
-    private String description;
-    private List<LedgerTransactionItem> items;
-
-    LedgerTransaction(String id, String date, String description) {
-        this.id = id;
-        this.date = date;
-        this.description = description;
-        this.items = new ArrayList<>();
-    }
-    LedgerTransaction(String date, String description) {
-        this(null, date, description);
-    }
-    void add_item(LedgerTransactionItem item) {
-        items.add(item);
-    }
-
-    public String getDate() {
-        return date;
-    }
-
-    public void setDate(String date) {
-        this.date = date;
-    }
-
-    public String getDescription() {
-        return description;
-    }
-
-    public void setDescription(String description) {
-        this.description = description;
-    }
-
-    Iterator<LedgerTransactionItem> getItemsIterator() {
-        return new Iterator<LedgerTransactionItem>() {
-            private int pointer = 0;
-            @Override
-            public boolean hasNext() {
-                return pointer < items.size();
-            }
-
-            @Override
-            public LedgerTransactionItem next() {
-                return hasNext() ? items.get(pointer++) : null;
-            }
-        };
-    }
-    public String getId() {
-        return id;
-    }
-
-    void insertInto(SQLiteDatabase db) {
-        db.execSQL("INSERT INTO transactions(id, date, " + "description) 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()});
-        }
-    }
-}
diff --git a/app/src/main/java/net/ktnx/mobileledger/LedgerTransactionItem.java b/app/src/main/java/net/ktnx/mobileledger/LedgerTransactionItem.java
deleted file mode 100644 (file)
index 3453a61..0000000
+++ /dev/null
@@ -1,70 +0,0 @@
-/*
- * 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;
-
-class LedgerTransactionItem {
-    private String accountName;
-    private float amount;
-    private boolean amountSet;
-    private String currency;
-
-    LedgerTransactionItem(String accountName, float amount) {
-        this(accountName, amount, null);
-    }
-    LedgerTransactionItem(String accountName, float amount, String currency) {
-        this.accountName = accountName;
-        this.amount = amount;
-        this.amountSet = true;
-        this.currency = currency;
-    }
-
-    public LedgerTransactionItem(String accountName) {
-        this.accountName = accountName;
-    }
-
-    public String getAccountName() {
-        return accountName;
-    }
-
-    public void setAccountName(String accountName) {
-        this.accountName = accountName;
-    }
-
-    public float getAmount() {
-        if (!amountSet)
-            throw new IllegalStateException("Account amount is not set");
-
-        return amount;
-    }
-
-    public void setAmount(float account_amount) {
-        this.amount = account_amount;
-        this.amountSet = true;
-    }
-
-    public void resetAmount() {
-        this.amountSet = false;
-    }
-
-    public boolean isAmountSet() {
-        return amountSet;
-    }
-    public String getCurrency() {
-        return currency;
-    }
-}
diff --git a/app/src/main/java/net/ktnx/mobileledger/MobileLedgerDatabase.java b/app/src/main/java/net/ktnx/mobileledger/MobileLedgerDatabase.java
deleted file mode 100644 (file)
index c7f4c5e..0000000
+++ /dev/null
@@ -1,157 +0,0 @@
-/*
- * 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.database.Cursor;
-import android.database.SQLException;
-import android.database.sqlite.SQLiteDatabase;
-import android.database.sqlite.SQLiteOpenHelper;
-import android.util.Log;
-
-import java.io.BufferedReader;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.InputStreamReader;
-import java.util.Locale;
-
-class MobileLedgerDatabase extends SQLiteOpenHelper implements AutoCloseable {
-    static final String DB_NAME = "mobile-ledger.db";
-    static final String ACCOUNTS_TABLE = "accounts";
-    static final String DESCRIPTION_HISTORY_TABLE = "description_history";
-    static final int LATEST_REVISION = 7;
-
-    final Context mContext;
-
-    public
-    MobileLedgerDatabase(Context context) {
-        super(context, DB_NAME, null, LATEST_REVISION);
-        Log.d("db", "creating helper instance");
-        mContext = context;
-    }
-
-    @Override
-    public
-    void onCreate(SQLiteDatabase db) {
-        Log.d("db", "onCreate called");
-        onUpgrade(db, -1, LATEST_REVISION);
-    }
-
-    @Override
-    public
-    void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
-        Log.d("db", "onUpgrade called");
-        for(int i = oldVersion+1; i <= newVersion; i++) applyRevision(db, i);
-    }
-
-    private void applyRevision(SQLiteDatabase db, int
-            rev_no) {
-        final Resources rm = mContext.getResources();
-        String rev_file = String.format(Locale.US, "sql_%d", rev_no);
-
-        int res_id = rm.getIdentifier(rev_file, "raw", mContext.getPackageName());
-        if (res_id == 0)
-            throw new SQLException(String.format(Locale.US, "No resource for revision %d", rev_no));
-        db.beginTransaction();
-        try (InputStream res = rm.openRawResource(res_id)) {
-            Log.d("db", "Applying revision " + String.valueOf(rev_no));
-            InputStreamReader isr = new InputStreamReader(res);
-            BufferedReader reader = new BufferedReader(isr);
-
-            String line;
-            while ((line = reader.readLine()) != null) {
-                db.execSQL(line);
-            }
-
-            db.setTransactionSuccessful();
-        }
-        catch (IOException e) {
-            Log.e("db", String.format("Error opening raw resource for revision %d", rev_no));
-            e.printStackTrace();
-        }
-        finally {
-            db.endTransaction();
-        }
-    }
-    int get_option_value(String name, int default_value) {
-        String s = get_option_value(name, String.valueOf(default_value));
-        try {
-            return Integer.parseInt(s);
-        }
-        catch (Exception e) {
-            return default_value;
-        }
-    }
-
-    long get_option_value(String name, long default_value) {
-        String s = get_option_value(name, String.valueOf(default_value));
-        try {
-            return Long.parseLong(s);
-        }
-        catch (Exception e) {
-            Log.d("db", "returning default long value of "+name, e);
-            return default_value;
-        }
-    }
-
-    String get_option_value(String name, String default_value) {
-        Log.d("db", "about to fetch option "+name);
-        try(SQLiteDatabase db = getReadableDatabase()) {
-            try (Cursor cursor = db
-                    .rawQuery("select value from options where name=?", new String[]{name}))
-            {
-                if (cursor.moveToFirst()) {
-                    String result = cursor.getString(0);
-
-                    if (result == null) result = default_value;
-
-                    Log.d("db", "option " + name + "=" + result);
-                    return result;
-                }
-                else return default_value;
-            }
-            catch (Exception e) {
-                Log.d("db", "returning default value for " + name, e);
-                return default_value;
-            }
-        }
-    }
-
-     void set_option_value(String name, String value) {
-        Log.d("db", "setting option "+name+"="+value);
-        try(SQLiteDatabase db = getWritableDatabase()) {
-            db.execSQL("insert or replace into options(name, value) values(?, ?);",
-                    new String[]{name, value});
-        }
-    }
-
-    void set_option_value(String name, long value) {
-        set_option_value(name, String.valueOf(value));
-    }
-    static long get_option_value(Context context, String name, long default_value) {
-        try(MobileLedgerDatabase db = new MobileLedgerDatabase(context)) {
-            return db.get_option_value(name, default_value);
-        }
-    }
-    static void set_option_value(Context context, String name, String value) {
-        try(MobileLedgerDatabase db = new MobileLedgerDatabase(context)) {
-            db.set_option_value(name, value);
-        }
-    }
-}
diff --git a/app/src/main/java/net/ktnx/mobileledger/NetworkUtil.java b/app/src/main/java/net/ktnx/mobileledger/NetworkUtil.java
deleted file mode 100644 (file)
index e311fcf..0000000
+++ /dev/null
@@ -1,48 +0,0 @@
-/*
- * 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.SharedPreferences;
-import android.util.Base64;
-import android.util.Log;
-
-import java.io.IOException;
-import java.net.HttpURLConnection;
-import java.net.URL;
-
-final class NetworkUtil {
-    static HttpURLConnection prepare_connection(SharedPreferences pref, String path) throws IOException {
-        final String backend_url = pref.getString("backend_url", "");
-        final boolean use_auth = pref.getBoolean("backend_use_http_auth", false);
-        Log.d("network", "Connecting to "+backend_url + "/" + path);
-        HttpURLConnection http = (HttpURLConnection) new URL(backend_url + "/" + path).openConnection();
-        if (use_auth) {
-            final String auth_user = pref.getString("backend_auth_user", "");
-            final String auth_password = pref.getString("backend_auth_password", "");
-            final byte[] bytes = (String.format("%s:%s", auth_user, auth_password)).getBytes("UTF-8");
-            final String value = Base64.encodeToString(bytes, Base64.DEFAULT);
-            http.setRequestProperty("Authorization", "Basic " + value);
-        }
-        http.setAllowUserInteraction(false);
-        http.setRequestProperty("Accept-Charset", "UTF-8");
-        http.setInstanceFollowRedirects(false);
-        http.setUseCaches(false);
-
-        return http;
-    }
-}
index 85c512096308567c9f96f85d88c77d33b2448aed..4950a1ef502451443767f186af2c4b642143a64a 100644 (file)
@@ -51,12 +51,17 @@ import android.widget.TableLayout;
 import android.widget.TableRow;
 import android.widget.TextView;
 
+import net.ktnx.mobileledger.async.SaveTransactionTask;
+import net.ktnx.mobileledger.async.TaskCallback;
+import net.ktnx.mobileledger.model.LedgerTransaction;
+import net.ktnx.mobileledger.model.LedgerTransactionItem;
+import net.ktnx.mobileledger.ui.DatePickerFragment;
+import net.ktnx.mobileledger.utils.MobileLedgerDatabase;
+
 import java.util.Date;
 import java.util.Objects;
 
 /*
- * TODO: auto-fill of transaction description
- *       if Android O's implementation won't work, add a custom one
  * TODO: nicer progress while transaction is submitted
  * TODO: latest transactions, maybe with browsing further in the past?
  * TODO: reports
diff --git a/app/src/main/java/net/ktnx/mobileledger/RetrieveAccountsTask.java b/app/src/main/java/net/ktnx/mobileledger/RetrieveAccountsTask.java
deleted file mode 100644 (file)
index 6e66453..0000000
+++ /dev/null
@@ -1,196 +0,0 @@
-/*
- * 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.SharedPreferences;
-import android.database.sqlite.SQLiteDatabase;
-import android.util.Log;
-
-import java.io.BufferedReader;
-import java.io.FileNotFoundException;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.InputStreamReader;
-import java.lang.ref.WeakReference;
-import java.net.HttpURLConnection;
-import java.net.MalformedURLException;
-import java.net.URLDecoder;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
-
-class RetrieveAccountsTask extends android.os.AsyncTask<Void, Integer, Void> {
-    int error;
-
-    private SharedPreferences pref;
-    WeakReference<AccountSummary> mContext;
-
-    RetrieveAccountsTask(WeakReference<AccountSummary> context) {
-        mContext = context;
-        error = 0;
-    }
-
-    void setPref(SharedPreferences pref) {
-        this.pref = pref;
-    }
-
-    protected Void doInBackground(Void... params) {
-        try {
-            HttpURLConnection http = NetworkUtil.prepare_connection( pref, "add");
-            publishProgress(0);
-            try(MobileLedgerDatabase dbh = new MobileLedgerDatabase(mContext.get())) {
-                try(SQLiteDatabase db = dbh.getWritableDatabase()) {
-                    try (InputStream resp = http.getInputStream()) {
-                        Log.d("update_accounts", String.valueOf(http.getResponseCode()));
-                        if (http.getResponseCode() != 200) {
-                            throw new IOException(
-                                    String.format("HTTP error: %d %s", http.getResponseCode(), http.getResponseMessage()));
-                        }
-                        else {
-                            if (db.inTransaction()) throw new AssertionError();
-
-                            db.beginTransaction();
-
-                            try {
-                                db.execSQL("update account_values set keep=0;");
-                                db.execSQL("update accounts set keep=0;");
-
-                                String line;
-                                String last_account_name = null;
-                                BufferedReader buf =
-                                        new BufferedReader(new InputStreamReader(resp, "UTF-8"));
-                                // %3A is '='
-                                Pattern account_name_re = Pattern.compile("/register\\?q=inacct%3A([a-zA-Z0-9%]+)\"");
-                                Pattern value_re = Pattern.compile(
-                                        "<span class=\"[^\"]*\\bamount\\b[^\"]*\">\\s*([-+]?[\\d.,]+)(?:\\s+(\\S+))?</span>");
-                                Pattern tr_re = Pattern.compile("</tr>");
-                                Pattern descriptions_line_re = Pattern.compile("\\bdescriptionsSuggester\\s*=\\s*new\\b");
-                                Pattern description_items_re = Pattern.compile("\"value\":\"([^\"]+)\"");
-                                int count = 0;
-                                while ((line = buf.readLine()) != null) {
-                                    Matcher 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("\"", "");
-                                        Log.d("account-parser", acct_name);
-
-                                        addAccount(db, acct_name);
-                                        publishProgress(++count);
-
-                                        last_account_name = acct_name;
-
-                                        continue;
-                                    }
-
-                                    Matcher tr_m = tr_re.matcher(line);
-                                    if (tr_m.find()) {
-                                        Log.d("account-parser", "<tr> - another account expected");
-                                        last_account_name = null;
-                                        continue;
-                                    }
-
-                                    if (last_account_name != null) {
-                                        m = value_re.matcher(line);
-                                        boolean match_found = false;
-                                        while (m.find()) {
-                                            match_found = true;
-                                            String value = m.group(1);
-                                            String currency = m.group(2);
-                                            if (currency == null) currency = "";
-                                            value = value.replace(',', '.');
-                                            Log.d("db", "curr=" + currency + ", value=" + value);
-                                            db.execSQL(
-                                                    "insert or replace into account_values(account, currency, value, keep) values(?, ?, ?, 1);",
-                                                    new Object[]{last_account_name, currency, Float.valueOf(value)
-                                                    });
-                                        }
-
-                                        if (match_found) continue;
-                                    }
-
-                                    m = descriptions_line_re.matcher(line);
-                                    if (m.find()) {
-                                        db.execSQL("update description_history set keep=0;");
-                                        m = description_items_re.matcher(line);
-                                        while (m.find()) {
-                                            String description = m.group(1);
-                                            if (description.isEmpty()) continue;
-
-                                            Log.d("db", String.format("Stored description: %s",
-                                                    description));
-                                            db.execSQL("insert or replace into description_history"
-                                                            + "(description, description_upper, keep) " + "values(?, ?, 1);",
-                                                    new Object[]{description, description.toUpperCase()
-                                                    });
-                                        }
-                                    }
-                                }
-
-                                db.execSQL("delete from account_values where keep=0;");
-                                db.execSQL("delete from accounts where keep=0;");
-//                        db.execSQL("delete from description_history where keep=0;");
-                                db.setTransactionSuccessful();
-                            }
-                            finally {
-                                db.endTransaction();
-                            }
-
-                        }
-                    }
-                }
-            }
-        } catch (MalformedURLException e) {
-            error = R.string.err_bad_backend_url;
-            e.printStackTrace();
-        }
-        catch (FileNotFoundException e) {
-            error = R.string.err_bad_auth;
-            e.printStackTrace();
-        }
-        catch (IOException e) {
-            error = R.string.err_net_io_error;
-            e.printStackTrace();
-        }
-        catch (Exception e) {
-            error = R.string.err_net_error;
-            e.printStackTrace();
-        }
-
-        return null;
-    }
-
-    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);
-    }
-    @Override
-    protected void onPostExecute(Void result) {
-        AccountSummary ctx = mContext.get();
-        if (ctx == null) return;
-        ctx.onAccountRefreshDone(this.error);
-    }
-
-}
diff --git a/app/src/main/java/net/ktnx/mobileledger/RetrieveTransactionsTask.java b/app/src/main/java/net/ktnx/mobileledger/RetrieveTransactionsTask.java
deleted file mode 100644 (file)
index 4131a21..0000000
+++ /dev/null
@@ -1,210 +0,0 @@
-/*
- * 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.SharedPreferences;
-import android.database.sqlite.SQLiteDatabase;
-import android.os.AsyncTask;
-
-import java.io.BufferedReader;
-import java.io.FileNotFoundException;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.InputStreamReader;
-import java.lang.ref.WeakReference;
-import java.net.HttpURLConnection;
-import java.net.MalformedURLException;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
-
-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;
-    }
-}
-
-class RetrieveTransactionsTask extends AsyncTask<Params, Integer, 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;
-    protected int error;
-    @Override
-    protected Void doInBackground(Params... params) {
-        try {
-            HttpURLConnection http =
-                    NetworkUtil.prepare_connection(params[0].getBackendPref(), "journal");
-            http.setAllowUserInteraction(false);
-            publishProgress(0);
-            Context ctx = contextRef.get();
-            if (ctx == null) return null;
-            try (MobileLedgerDatabase dbh = new MobileLedgerDatabase(ctx)) {
-                try (SQLiteDatabase db = dbh.getWritableDatabase()) {
-                    try (InputStream resp = http.getInputStream()) {
-                        if (http.getResponseCode() != 200) throw new IOException(
-                                String.format("HTTP error %d", http.getResponseCode()));
-                        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});
-                            }
-
-                            int state = ParserState.EXPECTING_JOURNAL;
-                            String line;
-                            BufferedReader buf =
-                                    new BufferedReader(new InputStreamReader(resp, "UTF-8"));
-
-                            int transactionCount = 0;
-                            String transactionId = null;
-                            LedgerTransaction transaction = null;
-                            while ((line = buf.readLine()) != null) {
-                                switch (state) {
-                                    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);
-                                        if (m.find()) {
-                                            transactionId = m.group(1);
-                                            state = ParserState.EXPECTING_TRANSACTION_DESCRIPTION;
-                                        }
-                                    }
-                                    case ParserState.EXPECTING_TRANSACTION_DESCRIPTION: {
-                                        Matcher m = transactionDescriptionPattern.matcher(line);
-                                        if (m.find()) {
-                                            if (transactionId == null)
-                                                throw new TransactionParserException(
-                                                        "Transaction Id is null while expecting description");
-
-                                            transaction =
-                                                    new LedgerTransaction(transactionId, m.group(1),
-                                                            m.group(2));
-                                            state = ParserState.EXPECTING_TRANSACTION_DETAILS;
-                                        }
-                                    }
-                                    case ParserState.EXPECTING_TRANSACTION_DETAILS: {
-                                        if (transaction == null)
-                                            throw new TransactionParserException(
-                                                    "Transaction is null while expecting details");
-                                        if (line.isEmpty()) {
-                                            // transaction data collected
-                                            transaction.insertInto(db);
-
-                                            state = ParserState.EXPECTING_TRANSACTION;
-                                            publishProgress(++transactionCount);
-                                        }
-                                        else {
-                                            Matcher m = transactionDetailsPattern.matcher(line);
-                                            if (m.find()) {
-                                                String acc_name = m.group(1);
-                                                String amount = m.group(2);
-                                                amount = amount.replace(',', '.');
-                                                transaction.add_item(
-                                                        new LedgerTransactionItem(acc_name,
-                                                                Float.valueOf(amount)));
-                                            }
-                                            else throw new IllegalStateException(String.format(
-                                                    "Can't" + " parse transaction details"));
-                                        }
-                                    }
-                                    default:
-                                        throw new RuntimeException(
-                                                String.format("Unknown " + "parser state %d",
-                                                        state));
-                                }
-                            }
-                            db.setTransactionSuccessful();
-                        }
-                        finally {
-                            db.endTransaction();
-                        }
-                    }
-                }
-            }
-        }
-        catch (MalformedURLException e) {
-            error = R.string.err_bad_backend_url;
-            e.printStackTrace();
-        }
-        catch (FileNotFoundException e) {
-            error = R.string.err_bad_auth;
-            e.printStackTrace();
-        }
-        catch (IOException e) {
-            error = R.string.err_net_io_error;
-            e.printStackTrace();
-        }
-        return null;
-    }
-    WeakReference<Context> getContextRef() {
-        return contextRef;
-    }
-
-    private class TransactionParserException extends IllegalStateException {
-        TransactionParserException(String message) {
-            super(message);
-        }
-    }
-
-    private class ParserState {
-        static final int EXPECTING_JOURNAL = 0;
-        static final int EXPECTING_TRANSACTION = 1;
-        static final int EXPECTING_TRANSACTION_DESCRIPTION = 2;
-        static final int EXPECTING_TRANSACTION_DETAILS = 3;
-    }
-}
diff --git a/app/src/main/java/net/ktnx/mobileledger/SaveTransactionTask.java b/app/src/main/java/net/ktnx/mobileledger/SaveTransactionTask.java
deleted file mode 100644 (file)
index 2e95a7d..0000000
+++ /dev/null
@@ -1,172 +0,0 @@
-/*
- * 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.SharedPreferences;
-import android.os.AsyncTask;
-import android.util.Log;
-
-import java.io.BufferedReader;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.InputStreamReader;
-import java.io.OutputStream;
-import java.net.HttpURLConnection;
-import java.util.Iterator;
-import java.util.List;
-import java.util.Locale;
-import java.util.Map;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
-
-import static java.lang.Thread.sleep;
-
-class SaveTransactionTask extends AsyncTask<LedgerTransaction, Void, Void> {
-    private final TaskCallback task_callback;
-    private String token;
-    private String session;
-    private String backend_url;
-    private LedgerTransaction ltr;
-    protected String error;
-
-    private SharedPreferences pref;
-    void setPref(SharedPreferences pref) {
-        this.pref = pref;
-    }
-
-    SaveTransactionTask(TaskCallback callback) {
-        task_callback = callback;
-    }
-    private boolean send_ok() throws IOException {
-        HttpURLConnection http = NetworkUtil.prepare_connection(pref, "add");
-        http.setRequestMethod("POST");
-        http.setRequestProperty("Content-Type", "application/x-www-form-urlencoded");
-        http.setRequestProperty("Accept", "*/*");
-        if ((session != null) && !session.isEmpty()) {
-            http.setRequestProperty("Cookie", String.format("_SESSION=%s", session));
-        }
-        http.setDoOutput(true);
-        http.setDoInput(true);
-
-        UrlEncodedFormData params = new UrlEncodedFormData();
-        params.add_pair("_formid", "identify-add");
-        if (token != null) params.add_pair("_token", token);
-        params.add_pair("date", ltr.getDate());
-        params.add_pair("description", ltr.getDescription());
-        {
-            Iterator<LedgerTransactionItem> items = ltr.getItemsIterator();
-            while (items.hasNext()) {
-                LedgerTransactionItem item = items.next();
-                params.add_pair("account", item.getAccountName());
-                if (item.isAmountSet())
-                    params.add_pair("amount", String.format(Locale.US, "%1.2f", item.getAmount()));
-                else params.add_pair("amount", "");
-            }
-        }
-
-        String body = params.toString();
-        http.addRequestProperty("Content-Length", String.valueOf(body.length()));
-
-        Log.d("network", "request header: " + http.getRequestProperties().toString());
-
-        try (OutputStream req = http.getOutputStream()) {
-            Log.d("network", "Request body: " + body);
-            req.write(body.getBytes("ASCII"));
-
-            try (InputStream resp = http.getInputStream()) {
-                Log.d("update_accounts", String.valueOf(http.getResponseCode()));
-                if (http.getResponseCode() == 303) {
-                    // everything is fine
-                    return true;
-                } else if (http.getResponseCode() == 200) {
-                    // get the new cookie
-                    {
-                        Pattern sess_cookie_re = Pattern.compile("_SESSION=([^;]+);.*");
-
-                        Map<String, List<String>> header = http.getHeaderFields();
-                        List<String> cookie_header = header.get("Set-Cookie");
-                        if (cookie_header != null) {
-                            String cookie = cookie_header.get(0);
-                            Matcher m = sess_cookie_re.matcher(cookie);
-                            if (m.matches()) {
-                                session = m.group(1);
-                                Log.d("network", "new session is " + session);
-                            } else {
-                                Log.d("network", "set-cookie: " + cookie);
-                                Log.w("network", "Response Set-Cookie headers is not a _SESSION one");
-                            }
-                        } else {
-                            Log.w("network", "Response has no Set-Cookie header");
-                        }
-                    }
-                    // the token needs to be updated
-                    BufferedReader reader = new BufferedReader(new InputStreamReader(resp));
-                    Pattern re = Pattern.compile("<input type=\"hidden\" name=\"_token\" value=\"([^\"]+)\">");
-                    String line;
-                    while ((line = reader.readLine()) != null) {
-                        //Log.d("dump", line);
-                        Matcher m = re.matcher(line);
-                        if (m.matches()) {
-                            token = m.group(1);
-                            Log.d("save-transaction", line);
-                            Log.d("save-transaction", "Token=" + token);
-                            return false;       // retry
-                        }
-                    }
-                    throw new IOException("Can't find _token string");
-                } else {
-                    throw new IOException(String.format("Error response code %d", http.getResponseCode()));
-                }
-            }
-        }
-    }
-
-    @Override
-    protected Void doInBackground(LedgerTransaction... ledgerTransactions) {
-        error = null;
-        try {
-            backend_url = pref.getString("backend_url", "");
-            ltr = ledgerTransactions[0];
-
-            int tried = 0;
-            while (!send_ok()) {
-                try {
-                    tried++;
-                    if (tried >= 2)
-                        throw new IOException(String.format("aborting after %d tries", tried));
-                    sleep(100);
-                }
-                catch (InterruptedException e) {
-                    e.printStackTrace();
-                }
-            }
-        }
-        catch (Exception e) {
-            e.printStackTrace();
-            error = e.getMessage();
-        }
-
-        return null;
-    }
-
-    @Override
-    protected void onPostExecute(Void aVoid) {
-        super.onPostExecute(aVoid);
-        task_callback.done(error);
-    }
-}
diff --git a/app/src/main/java/net/ktnx/mobileledger/TaskCallback.java b/app/src/main/java/net/ktnx/mobileledger/TaskCallback.java
deleted file mode 100644 (file)
index 5a6bacd..0000000
+++ /dev/null
@@ -1,22 +0,0 @@
-/*
- * 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;
-
-interface TaskCallback {
-    void done(String error);
-}
diff --git a/app/src/main/java/net/ktnx/mobileledger/UrlEncodedFormData.java b/app/src/main/java/net/ktnx/mobileledger/UrlEncodedFormData.java
deleted file mode 100644 (file)
index 1928b3c..0000000
+++ /dev/null
@@ -1,63 +0,0 @@
-/*
- * 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.support.annotation.NonNull;
-
-import java.io.UnsupportedEncodingException;
-import java.net.URLEncoder;
-import java.util.AbstractMap;
-import java.util.ArrayList;
-import java.util.List;
-
-class UrlEncodedFormData {
-    private List<AbstractMap.SimpleEntry<String,String>> pairs;
-
-    UrlEncodedFormData() {
-        pairs = new ArrayList<>();
-    }
-
-    void add_pair(String name, String value) {
-        pairs.add(new AbstractMap.SimpleEntry<String,String>(name, value));
-    }
-
-    @NonNull
-    public String toString() {
-        StringBuilder result = new StringBuilder();
-        boolean first = true;
-
-        for (AbstractMap.SimpleEntry<String,String> pair : pairs) {
-            if (first) {
-                first = false;
-            }
-            else {
-                result.append('&');
-            }
-
-            try {
-                result.append(URLEncoder.encode(pair.getKey(), "UTF-8"))
-                      .append('=')
-                      .append(URLEncoder.encode(pair.getValue(), "UTF-8"));
-            } catch (UnsupportedEncodingException e) {
-                e.printStackTrace();
-            }
-        }
-
-        return result.toString();
-    }
-}
diff --git a/app/src/main/java/net/ktnx/mobileledger/async/RetrieveAccountsTask.java b/app/src/main/java/net/ktnx/mobileledger/async/RetrieveAccountsTask.java
new file mode 100644 (file)
index 0000000..8117a19
--- /dev/null
@@ -0,0 +1,202 @@
+/*
+ * 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.async;
+
+import android.content.SharedPreferences;
+import android.database.sqlite.SQLiteDatabase;
+import android.util.Log;
+
+import net.ktnx.mobileledger.AccountSummary;
+import net.ktnx.mobileledger.R;
+import net.ktnx.mobileledger.model.LedgerAccount;
+import net.ktnx.mobileledger.utils.MobileLedgerDatabase;
+import net.ktnx.mobileledger.utils.NetworkUtil;
+
+import java.io.BufferedReader;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.lang.ref.WeakReference;
+import java.net.HttpURLConnection;
+import java.net.MalformedURLException;
+import java.net.URLDecoder;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public class RetrieveAccountsTask extends android.os.AsyncTask<Void, Integer, Void> {
+    int error;
+
+    private SharedPreferences pref;
+    WeakReference<AccountSummary> mContext;
+
+    public RetrieveAccountsTask(WeakReference<AccountSummary> context) {
+        mContext = context;
+        error = 0;
+    }
+
+    public void setPref(SharedPreferences pref) {
+        this.pref = pref;
+    }
+
+    protected Void doInBackground(Void... params) {
+        try {
+            HttpURLConnection http = NetworkUtil.prepare_connection( pref, "add");
+            publishProgress(0);
+            try(MobileLedgerDatabase dbh = new MobileLedgerDatabase(mContext.get())) {
+                try(SQLiteDatabase db = dbh.getWritableDatabase()) {
+                    try (InputStream resp = http.getInputStream()) {
+                        Log.d("update_accounts", String.valueOf(http.getResponseCode()));
+                        if (http.getResponseCode() != 200) {
+                            throw new IOException(
+                                    String.format("HTTP error: %d %s", http.getResponseCode(), http.getResponseMessage()));
+                        }
+                        else {
+                            if (db.inTransaction()) throw new AssertionError();
+
+                            db.beginTransaction();
+
+                            try {
+                                db.execSQL("update account_values set keep=0;");
+                                db.execSQL("update accounts set keep=0;");
+
+                                String line;
+                                String last_account_name = null;
+                                BufferedReader buf =
+                                        new BufferedReader(new InputStreamReader(resp, "UTF-8"));
+                                // %3A is '='
+                                Pattern account_name_re = Pattern.compile("/register\\?q=inacct%3A([a-zA-Z0-9%]+)\"");
+                                Pattern value_re = Pattern.compile(
+                                        "<span class=\"[^\"]*\\bamount\\b[^\"]*\">\\s*([-+]?[\\d.,]+)(?:\\s+(\\S+))?</span>");
+                                Pattern tr_re = Pattern.compile("</tr>");
+                                Pattern descriptions_line_re = Pattern.compile("\\bdescriptionsSuggester\\s*=\\s*new\\b");
+                                Pattern description_items_re = Pattern.compile("\"value\":\"([^\"]+)\"");
+                                int count = 0;
+                                while ((line = buf.readLine()) != null) {
+                                    Matcher 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("\"", "");
+                                        Log.d("account-parser", acct_name);
+
+                                        addAccount(db, acct_name);
+                                        publishProgress(++count);
+
+                                        last_account_name = acct_name;
+
+                                        continue;
+                                    }
+
+                                    Matcher tr_m = tr_re.matcher(line);
+                                    if (tr_m.find()) {
+                                        Log.d("account-parser", "<tr> - another account expected");
+                                        last_account_name = null;
+                                        continue;
+                                    }
+
+                                    if (last_account_name != null) {
+                                        m = value_re.matcher(line);
+                                        boolean match_found = false;
+                                        while (m.find()) {
+                                            match_found = true;
+                                            String value = m.group(1);
+                                            String currency = m.group(2);
+                                            if (currency == null) currency = "";
+                                            value = value.replace(',', '.');
+                                            Log.d("db", "curr=" + currency + ", value=" + value);
+                                            db.execSQL(
+                                                    "insert or replace into account_values(account, currency, value, keep) values(?, ?, ?, 1);",
+                                                    new Object[]{last_account_name, currency, Float.valueOf(value)
+                                                    });
+                                        }
+
+                                        if (match_found) continue;
+                                    }
+
+                                    m = descriptions_line_re.matcher(line);
+                                    if (m.find()) {
+                                        db.execSQL("update description_history set keep=0;");
+                                        m = description_items_re.matcher(line);
+                                        while (m.find()) {
+                                            String description = m.group(1);
+                                            if (description.isEmpty()) continue;
+
+                                            Log.d("db", String.format("Stored description: %s",
+                                                    description));
+                                            db.execSQL("insert or replace into description_history"
+                                                            + "(description, description_upper, keep) " + "values(?, ?, 1);",
+                                                    new Object[]{description, description.toUpperCase()
+                                                    });
+                                        }
+                                    }
+                                }
+
+                                db.execSQL("delete from account_values where keep=0;");
+                                db.execSQL("delete from accounts where keep=0;");
+//                        db.execSQL("delete from description_history where keep=0;");
+                                db.setTransactionSuccessful();
+                            }
+                            finally {
+                                db.endTransaction();
+                            }
+
+                        }
+                    }
+                }
+            }
+        } catch (MalformedURLException e) {
+            error = R.string.err_bad_backend_url;
+            e.printStackTrace();
+        }
+        catch (FileNotFoundException e) {
+            error = R.string.err_bad_auth;
+            e.printStackTrace();
+        }
+        catch (IOException e) {
+            error = R.string.err_net_io_error;
+            e.printStackTrace();
+        }
+        catch (Exception e) {
+            error = R.string.err_net_error;
+            e.printStackTrace();
+        }
+
+        return null;
+    }
+
+    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);
+    }
+    @Override
+    protected void onPostExecute(Void result) {
+        AccountSummary ctx = mContext.get();
+        if (ctx == null) return;
+        ctx.onAccountRefreshDone(this.error);
+    }
+
+}
diff --git a/app/src/main/java/net/ktnx/mobileledger/async/RetrieveTransactionsTask.java b/app/src/main/java/net/ktnx/mobileledger/async/RetrieveTransactionsTask.java
new file mode 100644 (file)
index 0000000..aeb49a5
--- /dev/null
@@ -0,0 +1,216 @@
+/*
+ * 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.async;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.database.sqlite.SQLiteDatabase;
+import android.os.AsyncTask;
+
+import net.ktnx.mobileledger.R;
+import net.ktnx.mobileledger.model.LedgerTransaction;
+import net.ktnx.mobileledger.model.LedgerTransactionItem;
+import net.ktnx.mobileledger.utils.MobileLedgerDatabase;
+import net.ktnx.mobileledger.utils.NetworkUtil;
+
+import java.io.BufferedReader;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.lang.ref.WeakReference;
+import java.net.HttpURLConnection;
+import java.net.MalformedURLException;
+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>");
+    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;
+    protected int error;
+    @Override
+    protected Void doInBackground(Params... params) {
+        try {
+            HttpURLConnection http =
+                    NetworkUtil.prepare_connection(params[0].getBackendPref(), "journal");
+            http.setAllowUserInteraction(false);
+            publishProgress(0);
+            Context ctx = contextRef.get();
+            if (ctx == null) return null;
+            try (MobileLedgerDatabase dbh = new MobileLedgerDatabase(ctx)) {
+                try (SQLiteDatabase db = dbh.getWritableDatabase()) {
+                    try (InputStream resp = http.getInputStream()) {
+                        if (http.getResponseCode() != 200) throw new IOException(
+                                String.format("HTTP error %d", http.getResponseCode()));
+                        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});
+                            }
+
+                            int state = ParserState.EXPECTING_JOURNAL;
+                            String line;
+                            BufferedReader buf =
+                                    new BufferedReader(new InputStreamReader(resp, "UTF-8"));
+
+                            int transactionCount = 0;
+                            String transactionId = null;
+                            LedgerTransaction transaction = null;
+                            while ((line = buf.readLine()) != null) {
+                                switch (state) {
+                                    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);
+                                        if (m.find()) {
+                                            transactionId = m.group(1);
+                                            state = ParserState.EXPECTING_TRANSACTION_DESCRIPTION;
+                                        }
+                                    }
+                                    case ParserState.EXPECTING_TRANSACTION_DESCRIPTION: {
+                                        Matcher m = transactionDescriptionPattern.matcher(line);
+                                        if (m.find()) {
+                                            if (transactionId == null)
+                                                throw new TransactionParserException(
+                                                        "Transaction Id is null while expecting description");
+
+                                            transaction =
+                                                    new LedgerTransaction(transactionId, m.group(1),
+                                                            m.group(2));
+                                            state = ParserState.EXPECTING_TRANSACTION_DETAILS;
+                                        }
+                                    }
+                                    case ParserState.EXPECTING_TRANSACTION_DETAILS: {
+                                        if (transaction == null)
+                                            throw new TransactionParserException(
+                                                    "Transaction is null while expecting details");
+                                        if (line.isEmpty()) {
+                                            // transaction data collected
+                                            transaction.insertInto(db);
+
+                                            state = ParserState.EXPECTING_TRANSACTION;
+                                            publishProgress(++transactionCount);
+                                        }
+                                        else {
+                                            Matcher m = transactionDetailsPattern.matcher(line);
+                                            if (m.find()) {
+                                                String acc_name = m.group(1);
+                                                String amount = m.group(2);
+                                                amount = amount.replace(',', '.');
+                                                transaction.add_item(
+                                                        new LedgerTransactionItem(acc_name,
+                                                                Float.valueOf(amount)));
+                                            }
+                                            else throw new IllegalStateException(String.format(
+                                                    "Can't" + " parse transaction details"));
+                                        }
+                                    }
+                                    default:
+                                        throw new RuntimeException(
+                                                String.format("Unknown " + "parser state %d",
+                                                        state));
+                                }
+                            }
+                            db.setTransactionSuccessful();
+                        }
+                        finally {
+                            db.endTransaction();
+                        }
+                    }
+                }
+            }
+        }
+        catch (MalformedURLException e) {
+            error = R.string.err_bad_backend_url;
+            e.printStackTrace();
+        }
+        catch (FileNotFoundException e) {
+            error = R.string.err_bad_auth;
+            e.printStackTrace();
+        }
+        catch (IOException e) {
+            error = R.string.err_net_io_error;
+            e.printStackTrace();
+        }
+        return null;
+    }
+    WeakReference<Context> getContextRef() {
+        return contextRef;
+    }
+
+    private class TransactionParserException extends IllegalStateException {
+        TransactionParserException(String message) {
+            super(message);
+        }
+    }
+
+    private class ParserState {
+        static final int EXPECTING_JOURNAL = 0;
+        static final int EXPECTING_TRANSACTION = 1;
+        static final int EXPECTING_TRANSACTION_DESCRIPTION = 2;
+        static final int EXPECTING_TRANSACTION_DETAILS = 3;
+    }
+}
diff --git a/app/src/main/java/net/ktnx/mobileledger/async/SaveTransactionTask.java b/app/src/main/java/net/ktnx/mobileledger/async/SaveTransactionTask.java
new file mode 100644 (file)
index 0000000..ac9c7c9
--- /dev/null
@@ -0,0 +1,177 @@
+/*
+ * 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.async;
+
+import android.content.SharedPreferences;
+import android.os.AsyncTask;
+import android.util.Log;
+
+import net.ktnx.mobileledger.model.LedgerTransaction;
+import net.ktnx.mobileledger.model.LedgerTransactionItem;
+import net.ktnx.mobileledger.utils.NetworkUtil;
+import net.ktnx.mobileledger.utils.UrlEncodedFormData;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.OutputStream;
+import java.net.HttpURLConnection;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import static java.lang.Thread.sleep;
+
+public class SaveTransactionTask extends AsyncTask<LedgerTransaction, Void, Void> {
+    private final TaskCallback task_callback;
+    private String token;
+    private String session;
+    private String backend_url;
+    private LedgerTransaction ltr;
+    protected String error;
+
+    private SharedPreferences pref;
+    public void setPref(SharedPreferences pref) {
+        this.pref = pref;
+    }
+
+    public SaveTransactionTask(TaskCallback callback) {
+        task_callback = callback;
+    }
+    private boolean send_ok() throws IOException {
+        HttpURLConnection http = NetworkUtil.prepare_connection(pref, "add");
+        http.setRequestMethod("POST");
+        http.setRequestProperty("Content-Type", "application/x-www-form-urlencoded");
+        http.setRequestProperty("Accept", "*/*");
+        if ((session != null) && !session.isEmpty()) {
+            http.setRequestProperty("Cookie", String.format("_SESSION=%s", session));
+        }
+        http.setDoOutput(true);
+        http.setDoInput(true);
+
+        UrlEncodedFormData params = new UrlEncodedFormData();
+        params.add_pair("_formid", "identify-add");
+        if (token != null) params.add_pair("_token", token);
+        params.add_pair("date", ltr.getDate());
+        params.add_pair("description", ltr.getDescription());
+        {
+            Iterator<LedgerTransactionItem> items = ltr.getItemsIterator();
+            while (items.hasNext()) {
+                LedgerTransactionItem item = items.next();
+                params.add_pair("account", item.getAccountName());
+                if (item.isAmountSet())
+                    params.add_pair("amount", String.format(Locale.US, "%1.2f", item.getAmount()));
+                else params.add_pair("amount", "");
+            }
+        }
+
+        String body = params.toString();
+        http.addRequestProperty("Content-Length", String.valueOf(body.length()));
+
+        Log.d("network", "request header: " + http.getRequestProperties().toString());
+
+        try (OutputStream req = http.getOutputStream()) {
+            Log.d("network", "Request body: " + body);
+            req.write(body.getBytes("ASCII"));
+
+            try (InputStream resp = http.getInputStream()) {
+                Log.d("update_accounts", String.valueOf(http.getResponseCode()));
+                if (http.getResponseCode() == 303) {
+                    // everything is fine
+                    return true;
+                } else if (http.getResponseCode() == 200) {
+                    // get the new cookie
+                    {
+                        Pattern sess_cookie_re = Pattern.compile("_SESSION=([^;]+);.*");
+
+                        Map<String, List<String>> header = http.getHeaderFields();
+                        List<String> cookie_header = header.get("Set-Cookie");
+                        if (cookie_header != null) {
+                            String cookie = cookie_header.get(0);
+                            Matcher m = sess_cookie_re.matcher(cookie);
+                            if (m.matches()) {
+                                session = m.group(1);
+                                Log.d("network", "new session is " + session);
+                            } else {
+                                Log.d("network", "set-cookie: " + cookie);
+                                Log.w("network", "Response Set-Cookie headers is not a _SESSION one");
+                            }
+                        } else {
+                            Log.w("network", "Response has no Set-Cookie header");
+                        }
+                    }
+                    // the token needs to be updated
+                    BufferedReader reader = new BufferedReader(new InputStreamReader(resp));
+                    Pattern re = Pattern.compile("<input type=\"hidden\" name=\"_token\" value=\"([^\"]+)\">");
+                    String line;
+                    while ((line = reader.readLine()) != null) {
+                        //Log.d("dump", line);
+                        Matcher m = re.matcher(line);
+                        if (m.matches()) {
+                            token = m.group(1);
+                            Log.d("save-transaction", line);
+                            Log.d("save-transaction", "Token=" + token);
+                            return false;       // retry
+                        }
+                    }
+                    throw new IOException("Can't find _token string");
+                } else {
+                    throw new IOException(String.format("Error response code %d", http.getResponseCode()));
+                }
+            }
+        }
+    }
+
+    @Override
+    protected Void doInBackground(LedgerTransaction... ledgerTransactions) {
+        error = null;
+        try {
+            backend_url = pref.getString("backend_url", "");
+            ltr = ledgerTransactions[0];
+
+            int tried = 0;
+            while (!send_ok()) {
+                try {
+                    tried++;
+                    if (tried >= 2)
+                        throw new IOException(String.format("aborting after %d tries", tried));
+                    sleep(100);
+                }
+                catch (InterruptedException e) {
+                    e.printStackTrace();
+                }
+            }
+        }
+        catch (Exception e) {
+            e.printStackTrace();
+            error = e.getMessage();
+        }
+
+        return null;
+    }
+
+    @Override
+    protected void onPostExecute(Void aVoid) {
+        super.onPostExecute(aVoid);
+        task_callback.done(error);
+    }
+}
diff --git a/app/src/main/java/net/ktnx/mobileledger/async/TaskCallback.java b/app/src/main/java/net/ktnx/mobileledger/async/TaskCallback.java
new file mode 100644 (file)
index 0000000..31cdc8d
--- /dev/null
@@ -0,0 +1,22 @@
+/*
+ * 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.async;
+
+public interface TaskCallback {
+    void done(String error);
+}
diff --git a/app/src/main/java/net/ktnx/mobileledger/model/LedgerAccount.java b/app/src/main/java/net/ktnx/mobileledger/model/LedgerAccount.java
new file mode 100644 (file)
index 0000000..4cf08f9
--- /dev/null
@@ -0,0 +1,130 @@
+/*
+ * 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.model;
+
+import android.support.annotation.NonNull;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public class LedgerAccount {
+    private String name;
+    private String shortName;
+    private int level;
+    private String parentName;
+    private boolean hidden;
+    private boolean hiddenToBe;
+    private List<LedgerAmount> amounts;
+    static Pattern higher_account = Pattern.compile("^[^:]+:");
+
+    public LedgerAccount(String name) {
+        this.setName(name);
+        hidden = false;
+    }
+
+    public boolean isHidden() {
+        return hidden;
+    }
+
+    public void setHidden(boolean hidden) {
+        this.hidden = hidden;
+    }
+
+    public LedgerAccount(String name, float amount) {
+        this.setName(name);
+        this.hidden = false;
+        this.amounts = new ArrayList<LedgerAmount>();
+        this.addAmount(amount);
+    }
+
+    public void setName(String name) {
+        this.name = name;
+        stripName();
+    }
+
+    private void stripName() {
+        level = 0;
+        shortName = name;
+        StringBuilder parentBuilder = new StringBuilder();
+        while (true) {
+            Matcher m = higher_account.matcher(shortName);
+            if (m.find()) {
+                level++;
+                parentBuilder.append(m.group(0));
+                shortName = m.replaceFirst("");
+            }
+            else break;
+        }
+        if (parentBuilder.length() > 0)
+            parentName = parentBuilder.substring(0, parentBuilder.length() - 1);
+        else parentName = null;
+    }
+
+    public String getName() {
+        return name;
+    }
+
+    public void addAmount(float amount, String currency) {
+        if (amounts == null ) amounts = new ArrayList<>();
+        amounts.add(new LedgerAmount(amount, currency));
+    }
+    public void addAmount(float amount) {
+        this.addAmount(amount, null);
+    }
+
+    public String getAmountsString() {
+        if ((amounts == null) || amounts.isEmpty()) return "";
+
+        StringBuilder builder = new StringBuilder();
+        for( LedgerAmount amount : amounts ) {
+            String amt = amount.toString();
+            if (builder.length() > 0) builder.append('\n');
+            builder.append(amt);
+        }
+
+        return builder.toString();
+    }
+
+    public int getLevel() {
+        return level;
+    }
+
+    @NonNull
+    public String getShortName() {
+        return shortName;
+    }
+
+    public String getParentName() {
+        return parentName;
+    }
+    public void togglehidden() {
+        hidden = !hidden;
+    }
+
+    public boolean isHiddenToBe() {
+        return hiddenToBe;
+    }
+    public void setHiddenToBe(boolean hiddenToBe) {
+        this.hiddenToBe = hiddenToBe;
+    }
+    public void toggleHiddenToBe() {
+        setHiddenToBe(!hiddenToBe);
+    }
+}
diff --git a/app/src/main/java/net/ktnx/mobileledger/model/LedgerAmount.java b/app/src/main/java/net/ktnx/mobileledger/model/LedgerAmount.java
new file mode 100644 (file)
index 0000000..6eb4ffb
--- /dev/null
@@ -0,0 +1,45 @@
+/*
+ * 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.model;
+
+import android.annotation.SuppressLint;
+import android.support.annotation.NonNull;
+
+public class LedgerAmount {
+    private String currency;
+    private float amount;
+
+    public
+    LedgerAmount(float amount, @NonNull String currency) {
+        this.currency = currency;
+        this.amount = amount;
+    }
+
+    public
+    LedgerAmount(float amount) {
+        this.amount = amount;
+        this.currency = null;
+    }
+
+    @SuppressLint("DefaultLocale")
+    @NonNull
+    public String toString() {
+        if (currency == null) return String.format("%,1.2f", amount);
+        else return String.format("%s %,1.2f", currency, amount);
+    }
+}
diff --git a/app/src/main/java/net/ktnx/mobileledger/model/LedgerTransaction.java b/app/src/main/java/net/ktnx/mobileledger/model/LedgerTransaction.java
new file mode 100644 (file)
index 0000000..a103ab3
--- /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.model;
+
+import android.database.sqlite.SQLiteDatabase;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+
+public class LedgerTransaction {
+    private String id;
+    private String date;
+    private String description;
+    private List<LedgerTransactionItem> items;
+
+    public LedgerTransaction(String id, String date, String description) {
+        this.id = id;
+        this.date = date;
+        this.description = description;
+        this.items = new ArrayList<>();
+    }
+    public LedgerTransaction(String date, String description) {
+        this(null, date, description);
+    }
+    public void add_item(LedgerTransactionItem item) {
+        items.add(item);
+    }
+
+    public String getDate() {
+        return date;
+    }
+
+    public void setDate(String date) {
+        this.date = date;
+    }
+
+    public String getDescription() {
+        return description;
+    }
+
+    public void setDescription(String description) {
+        this.description = description;
+    }
+
+    public Iterator<LedgerTransactionItem> getItemsIterator() {
+        return new Iterator<LedgerTransactionItem>() {
+            private int pointer = 0;
+            @Override
+            public boolean hasNext() {
+                return pointer < items.size();
+            }
+
+            @Override
+            public LedgerTransactionItem next() {
+                return hasNext() ? items.get(pointer++) : null;
+            }
+        };
+    }
+    public String getId() {
+        return id;
+    }
+
+    public void insertInto(SQLiteDatabase db) {
+        db.execSQL("INSERT INTO transactions(id, date, " + "description) 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()});
+        }
+    }
+}
diff --git a/app/src/main/java/net/ktnx/mobileledger/model/LedgerTransactionItem.java b/app/src/main/java/net/ktnx/mobileledger/model/LedgerTransactionItem.java
new file mode 100644 (file)
index 0000000..ff19a45
--- /dev/null
@@ -0,0 +1,70 @@
+/*
+ * 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.model;
+
+public class LedgerTransactionItem {
+    private String accountName;
+    private float amount;
+    private boolean amountSet;
+    private String currency;
+
+    public LedgerTransactionItem(String accountName, float amount) {
+        this(accountName, amount, null);
+    }
+    public LedgerTransactionItem(String accountName, float amount, String currency) {
+        this.accountName = accountName;
+        this.amount = amount;
+        this.amountSet = true;
+        this.currency = currency;
+    }
+
+    public LedgerTransactionItem(String accountName) {
+        this.accountName = accountName;
+    }
+
+    public String getAccountName() {
+        return accountName;
+    }
+
+    public void setAccountName(String accountName) {
+        this.accountName = accountName;
+    }
+
+    public float getAmount() {
+        if (!amountSet)
+            throw new IllegalStateException("Account amount is not set");
+
+        return amount;
+    }
+
+    public void setAmount(float account_amount) {
+        this.amount = account_amount;
+        this.amountSet = true;
+    }
+
+    public void resetAmount() {
+        this.amountSet = false;
+    }
+
+    public boolean isAmountSet() {
+        return amountSet;
+    }
+    public String getCurrency() {
+        return currency;
+    }
+}
diff --git a/app/src/main/java/net/ktnx/mobileledger/ui/DatePickerFragment.java b/app/src/main/java/net/ktnx/mobileledger/ui/DatePickerFragment.java
new file mode 100644 (file)
index 0000000..f801bda
--- /dev/null
@@ -0,0 +1,113 @@
+/*
+ * 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.ui;
+
+import android.annotation.TargetApi;
+import android.app.DatePickerDialog;
+import android.app.Dialog;
+import android.os.Build;
+import android.os.Bundle;
+import android.support.annotation.NonNull;
+import android.support.v7.app.AppCompatDialogFragment;
+import android.widget.DatePicker;
+import android.widget.TextView;
+
+import net.ktnx.mobileledger.R;
+
+import java.util.Calendar;
+import java.util.GregorianCalendar;
+import java.util.Locale;
+import java.util.Objects;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public class DatePickerFragment extends AppCompatDialogFragment
+implements DatePickerDialog.OnDateSetListener, DatePicker.OnDateChangedListener
+{
+    @NonNull
+    @Override
+    public Dialog onCreateDialog(Bundle savedInstanceState) {
+        final Calendar c = GregorianCalendar.getInstance();
+        int year = c.get(GregorianCalendar.YEAR);
+        int month = c.get(GregorianCalendar.MONTH);
+        int day = c.get(GregorianCalendar.DAY_OF_MONTH);
+        TextView date = Objects.requireNonNull(getActivity()).findViewById(R.id.new_transaction_date);
+
+        CharSequence present = date.getText();
+
+        Pattern re_mon_day = Pattern.compile("^\\s*(\\d+)\\s*/\\s*(\\d+)\\s*$");
+        Matcher m_mon_day = re_mon_day.matcher(present);
+
+        if (m_mon_day.matches()) {
+            month = Integer.parseInt(m_mon_day.group(1))-1;
+            day = Integer.parseInt(m_mon_day.group(2));
+        }
+        else {
+            Pattern re_day = Pattern.compile("^\\s*(\\d{1,2})\\s*$");
+            Matcher m_day = re_day.matcher(present);
+            if (m_day.matches()) {
+                day = Integer.parseInt(m_day.group(1));
+            }
+        }
+
+        DatePickerDialog dpd =  new DatePickerDialog(Objects.requireNonNull(getActivity()), this, year, month, day);
+        // quicker date selection available in API 26
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+            DatePicker dp = dpd.getDatePicker();
+            dp.setOnDateChangedListener(this);
+        }
+
+        return dpd;
+    }
+
+    @TargetApi(Build.VERSION_CODES.O)
+    public void onDateSet(DatePicker view, int year, int month, int day) {
+        TextView date = Objects.requireNonNull(getActivity()).findViewById(R.id.new_transaction_date);
+
+        final Calendar c = GregorianCalendar.getInstance();
+        if ( c.get(GregorianCalendar.YEAR) == year && c.get(GregorianCalendar.MONTH) == month) {
+            date.setText(String.format(Locale.US, "%d", day));
+        }
+        else {
+            date.setText(String.format(Locale.US, "%d/%d", month+1, day));
+        }
+
+        TextView description = Objects.requireNonNull(getActivity())
+                .findViewById(R.id.new_transaction_description);
+        description.requestFocus();
+    }
+
+    @Override
+    public void onDateChanged(DatePicker view, int year, int monthOfYear, int dayOfMonth) {
+        TextView date = Objects.requireNonNull(getActivity()).findViewById(R.id.new_transaction_date);
+
+        final Calendar c = GregorianCalendar.getInstance();
+        if ( c.get(GregorianCalendar.YEAR) == year && c.get(GregorianCalendar.MONTH) == monthOfYear) {
+            date.setText(String.format(Locale.US, "%d", dayOfMonth));
+        }
+        else {
+            date.setText(String.format(Locale.US, "%d/%d", monthOfYear+1, dayOfMonth));
+        }
+
+        TextView description = Objects.requireNonNull(getActivity())
+                .findViewById(R.id.new_transaction_description);
+        description.requestFocus();
+
+        this.dismiss();
+    }
+}
diff --git a/app/src/main/java/net/ktnx/mobileledger/utils/DimensionUtils.java b/app/src/main/java/net/ktnx/mobileledger/utils/DimensionUtils.java
new file mode 100644 (file)
index 0000000..14c8979
--- /dev/null
@@ -0,0 +1,29 @@
+/*
+ * 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 android.content.Context;
+import android.util.TypedValue;
+
+public class DimensionUtils {
+    public static int dp2px(Context context, float dp) {
+        return Math.round(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp,
+               context.getResources().getDisplayMetrics()));
+    }
+
+}
diff --git a/app/src/main/java/net/ktnx/mobileledger/utils/MobileLedgerDatabase.java b/app/src/main/java/net/ktnx/mobileledger/utils/MobileLedgerDatabase.java
new file mode 100644 (file)
index 0000000..cae65b0
--- /dev/null
@@ -0,0 +1,157 @@
+/*
+ * 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 android.content.Context;
+import android.content.res.Resources;
+import android.database.Cursor;
+import android.database.SQLException;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteOpenHelper;
+import android.util.Log;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.util.Locale;
+
+public class MobileLedgerDatabase extends SQLiteOpenHelper implements AutoCloseable {
+    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;
+
+    private final Context mContext;
+
+    public
+    MobileLedgerDatabase(Context context) {
+        super(context, DB_NAME, null, LATEST_REVISION);
+        Log.d("db", "creating helper instance");
+        mContext = context;
+    }
+
+    @Override
+    public
+    void onCreate(SQLiteDatabase db) {
+        Log.d("db", "onCreate called");
+        onUpgrade(db, -1, LATEST_REVISION);
+    }
+
+    @Override
+    public
+    void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
+        Log.d("db", "onUpgrade called");
+        for(int i = oldVersion+1; i <= newVersion; i++) applyRevision(db, i);
+    }
+
+    private void applyRevision(SQLiteDatabase db, int
+            rev_no) {
+        final Resources rm = mContext.getResources();
+        String rev_file = String.format(Locale.US, "sql_%d", rev_no);
+
+        int res_id = rm.getIdentifier(rev_file, "raw", mContext.getPackageName());
+        if (res_id == 0)
+            throw new SQLException(String.format(Locale.US, "No resource for revision %d", rev_no));
+        db.beginTransaction();
+        try (InputStream res = rm.openRawResource(res_id)) {
+            Log.d("db", "Applying revision " + String.valueOf(rev_no));
+            InputStreamReader isr = new InputStreamReader(res);
+            BufferedReader reader = new BufferedReader(isr);
+
+            String line;
+            while ((line = reader.readLine()) != null) {
+                db.execSQL(line);
+            }
+
+            db.setTransactionSuccessful();
+        }
+        catch (IOException e) {
+            Log.e("db", String.format("Error opening raw resource for revision %d", rev_no));
+            e.printStackTrace();
+        }
+        finally {
+            db.endTransaction();
+        }
+    }
+    public int get_option_value(String name, int default_value) {
+        String s = get_option_value(name, String.valueOf(default_value));
+        try {
+            return Integer.parseInt(s);
+        }
+        catch (Exception e) {
+            return default_value;
+        }
+    }
+
+    public long get_option_value(String name, long default_value) {
+        String s = get_option_value(name, String.valueOf(default_value));
+        try {
+            return Long.parseLong(s);
+        }
+        catch (Exception e) {
+            Log.d("db", "returning default long value of "+name, e);
+            return default_value;
+        }
+    }
+
+    public String get_option_value(String name, String default_value) {
+        Log.d("db", "about to fetch option "+name);
+        try(SQLiteDatabase db = getReadableDatabase()) {
+            try (Cursor cursor = db
+                    .rawQuery("select value from options where name=?", new String[]{name}))
+            {
+                if (cursor.moveToFirst()) {
+                    String result = cursor.getString(0);
+
+                    if (result == null) result = default_value;
+
+                    Log.d("db", "option " + name + "=" + result);
+                    return result;
+                }
+                else return default_value;
+            }
+            catch (Exception e) {
+                Log.d("db", "returning default value for " + name, e);
+                return default_value;
+            }
+        }
+    }
+
+     public void set_option_value(String name, String value) {
+        Log.d("db", "setting option "+name+"="+value);
+        try(SQLiteDatabase db = getWritableDatabase()) {
+            db.execSQL("insert or replace into options(name, value) values(?, ?);",
+                    new String[]{name, value});
+        }
+    }
+
+    public void set_option_value(String name, long value) {
+        set_option_value(name, String.valueOf(value));
+    }
+    public static long get_option_value(Context context, String name, long default_value) {
+        try(MobileLedgerDatabase db = new MobileLedgerDatabase(context)) {
+            return db.get_option_value(name, default_value);
+        }
+    }
+    public static void set_option_value(Context context, String name, String value) {
+        try(MobileLedgerDatabase db = new MobileLedgerDatabase(context)) {
+            db.set_option_value(name, value);
+        }
+    }
+}
diff --git a/app/src/main/java/net/ktnx/mobileledger/utils/NetworkUtil.java b/app/src/main/java/net/ktnx/mobileledger/utils/NetworkUtil.java
new file mode 100644 (file)
index 0000000..8386bbf
--- /dev/null
@@ -0,0 +1,49 @@
+/*
+ * 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 android.content.SharedPreferences;
+import android.util.Base64;
+import android.util.Log;
+
+import java.io.IOException;
+import java.net.HttpURLConnection;
+import java.net.URL;
+
+public final class NetworkUtil {
+    public static HttpURLConnection prepare_connection(SharedPreferences pref, String path) throws
+            IOException {
+        final String backend_url = pref.getString("backend_url", "");
+        final boolean use_auth = pref.getBoolean("backend_use_http_auth", false);
+        Log.d("network", "Connecting to "+backend_url + "/" + path);
+        HttpURLConnection http = (HttpURLConnection) new URL(backend_url + "/" + path).openConnection();
+        if (use_auth) {
+            final String auth_user = pref.getString("backend_auth_user", "");
+            final String auth_password = pref.getString("backend_auth_password", "");
+            final byte[] bytes = (String.format("%s:%s", auth_user, auth_password)).getBytes("UTF-8");
+            final String value = Base64.encodeToString(bytes, Base64.DEFAULT);
+            http.setRequestProperty("Authorization", "Basic " + value);
+        }
+        http.setAllowUserInteraction(false);
+        http.setRequestProperty("Accept-Charset", "UTF-8");
+        http.setInstanceFollowRedirects(false);
+        http.setUseCaches(false);
+
+        return http;
+    }
+}
diff --git a/app/src/main/java/net/ktnx/mobileledger/utils/UrlEncodedFormData.java b/app/src/main/java/net/ktnx/mobileledger/utils/UrlEncodedFormData.java
new file mode 100644 (file)
index 0000000..5d69da2
--- /dev/null
@@ -0,0 +1,63 @@
+/*
+ * 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 android.support.annotation.NonNull;
+
+import java.io.UnsupportedEncodingException;
+import java.net.URLEncoder;
+import java.util.AbstractMap;
+import java.util.ArrayList;
+import java.util.List;
+
+public class UrlEncodedFormData {
+    private List<AbstractMap.SimpleEntry<String,String>> pairs;
+
+    public UrlEncodedFormData() {
+        pairs = new ArrayList<>();
+    }
+
+    public void add_pair(String name, String value) {
+        pairs.add(new AbstractMap.SimpleEntry<String,String>(name, value));
+    }
+
+    @NonNull
+    public String toString() {
+        StringBuilder result = new StringBuilder();
+        boolean first = true;
+
+        for (AbstractMap.SimpleEntry<String,String> pair : pairs) {
+            if (first) {
+                first = false;
+            }
+            else {
+                result.append('&');
+            }
+
+            try {
+                result.append(URLEncoder.encode(pair.getKey(), "UTF-8"))
+                      .append('=')
+                      .append(URLEncoder.encode(pair.getValue(), "UTF-8"));
+            } catch (UnsupportedEncodingException e) {
+                e.printStackTrace();
+            }
+        }
+
+        return result.toString();
+    }
+}