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;
task.execute();
}
- void onAccountRefreshDone(int error) {
+ public void onAccountRefreshDone(int error) {
SwipeRefreshLayout srl = findViewById(R.id.account_swiper);
srl.setRefreshing(false);
if (error != 0) {
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;
+++ /dev/null
-/*
- * 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();
- }
-}
+++ /dev/null
-/*
- * 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()));
- }
-
-}
+++ /dev/null
-/*
- * 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);
- }
-}
+++ /dev/null
-/*
- * 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);
- }
-}
+++ /dev/null
-/*
- * 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()});
- }
- }
-}
+++ /dev/null
-/*
- * 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;
- }
-}
+++ /dev/null
-/*
- * 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);
- }
- }
-}
+++ /dev/null
-/*
- * 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;
- }
-}
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
+++ /dev/null
-/*
- * 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);
- }
-
-}
+++ /dev/null
-/*
- * 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;
- }
-}
+++ /dev/null
-/*
- * 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);
- }
-}
+++ /dev/null
-/*
- * 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);
-}
+++ /dev/null
-/*
- * 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();
- }
-}
--- /dev/null
+/*
+ * 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);
+ }
+
+}
--- /dev/null
+/*
+ * 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;
+ }
+}
--- /dev/null
+/*
+ * 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);
+ }
+}
--- /dev/null
+/*
+ * 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);
+}
--- /dev/null
+/*
+ * 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);
+ }
+}
--- /dev/null
+/*
+ * 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);
+ }
+}
--- /dev/null
+/*
+ * 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()});
+ }
+ }
+}
--- /dev/null
+/*
+ * 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;
+ }
+}
--- /dev/null
+/*
+ * 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();
+ }
+}
--- /dev/null
+/*
+ * 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()));
+ }
+
+}
--- /dev/null
+/*
+ * 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);
+ }
+ }
+}
--- /dev/null
+/*
+ * 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;
+ }
+}
--- /dev/null
+/*
+ * 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();
+ }
+}