import android.os.OperationCanceledException;
import android.util.Log;
-import net.ktnx.mobileledger.R;
import net.ktnx.mobileledger.model.Data;
import net.ktnx.mobileledger.model.LedgerAccount;
import net.ktnx.mobileledger.model.LedgerTransaction;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URLDecoder;
+import java.text.ParseException;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
public class RetrieveTransactionsTask
- extends AsyncTask<Void, RetrieveTransactionsTask.Progress, Void> {
+ extends AsyncTask<Void, RetrieveTransactionsTask.Progress, String> {
private static final int MATCHING_TRANSACTIONS_LIMIT = 50;
private static final Pattern reComment = Pattern.compile("^\\s*;");
private static final Pattern reTransactionStart = Pattern.compile("<tr class=\"title\" " +
private WeakReference<MainActivity> contextRef;
private int error;
// %3A is '='
- private boolean success;
private Pattern reAccountName = Pattern.compile("/register\\?q=inacct%3A([a-zA-Z0-9%]+)\"");
private Pattern reAccountValue = Pattern.compile(
"<span class=\"[^\"]*\\bamount\\b[^\"]*\">\\s*([-+]?[\\d.,]+)(?:\\s+(\\S+))?</span>");
context.onRetrieveStart();
}
@Override
- protected void onPostExecute(Void aVoid) {
- super.onPostExecute(aVoid);
+ protected void onPostExecute(String error) {
+ super.onPostExecute(error);
MainActivity context = getContext();
if (context == null) return;
- context.onRetrieveDone(success);
+ context.onRetrieveDone(error);
}
@Override
protected void onCancelled() {
super.onCancelled();
MainActivity context = getContext();
if (context == null) return;
- context.onRetrieveDone(false);
+ context.onRetrieveDone(null);
}
@SuppressLint("DefaultLocale")
@Override
- protected Void doInBackground(Void... params) {
+ protected String doInBackground(Void... params) {
MobileLedgerProfile profile = Data.profile.get();
Progress progress = new Progress();
int maxTransactionId = Progress.INDETERMINATE;
- success = false;
ArrayList<LedgerAccount> accountList = new ArrayList<>();
HashMap<String, Void> accountNames = new HashMap<>();
LedgerAccount lastAccount = null;
m = reEnd.matcher(line);
if (m.find()) {
L("--- transaction value complete ---");
- success = true;
break LINES;
}
break;
"Transaction Id is 0 while expecting " +
"description");
- transaction =
- new LedgerTransaction(transactionId, m.group(1),
- m.group(2));
+ String date = m.group(1);
+ try {
+ int equalsIndex = date.indexOf('=');
+ if (equalsIndex >= 0)
+ date = date.substring(equalsIndex + 1);
+ transaction = new LedgerTransaction(transactionId, date,
+ m.group(2));
+ }
+ catch (ParseException e) {
+ e.printStackTrace();
+ return String.format("Error parsing date '%s'", date);
+ }
state = ParserState.EXPECTING_TRANSACTION_DETAILS;
L(String.format("transaction %d created for %s (%s) →" +
- " expecting details", transactionId,
- m.group(1), m.group(2)));
+ " expecting details", transactionId, date,
+ m.group(2)));
}
break;
new Object[]{profile.getUuid(),
transaction.getId()
});
- success = true;
progress.setTotal(progress.getProgress());
publishProgress(progress);
break LINES;
profile.setLongOption(MLDB.OPT_LAST_SCRAPE, now.getTime());
Data.lastUpdateDate.set(now);
TransactionListViewModel.scheduleTransactionListReload();
+
+ return null;
}
finally {
db.endTransaction();
}
}
catch (MalformedURLException e) {
- error = R.string.err_bad_backend_url;
e.printStackTrace();
+ return "Invalid server URL";
}
catch (FileNotFoundException e) {
- error = R.string.err_bad_auth;
e.printStackTrace();
+ return "Invalid user name or password";
}
catch (IOException e) {
- error = R.string.err_net_io_error;
e.printStackTrace();
+ return "Network error";
}
catch (OperationCanceledException e) {
- error = R.string.err_cancelled;
e.printStackTrace();
+ return "Operation cancelled";
}
finally {
Data.backgroundTaskCount.decrementAndGet();
}
- return null;
}
private MainActivity getContext() {
return contextRef.get();
import net.ktnx.mobileledger.utils.Globals;
import net.ktnx.mobileledger.utils.MLDB;
+import java.text.ParseException;
import java.util.ArrayList;
import java.util.Date;
-import java.util.List;
-public class UpdateTransactionsTask extends AsyncTask<String, Void, List<TransactionListItem>> {
- protected List<TransactionListItem> doInBackground(String[] filterAccName) {
+public class UpdateTransactionsTask extends AsyncTask<String, Void, String> {
+ protected String doInBackground(String[] filterAccName) {
Data.backgroundTaskCount.incrementAndGet();
String profile_uuid = Data.profile.get().getUuid();
try {
Log.d("UTT", "transaction list value updated");
}
- return newList;
+ return null;
+ }
+ catch (ParseException e) {
+ return String.format("Error parsing stored date '%s'", e.getMessage());
}
finally {
Data.backgroundTaskCount.decrementAndGet();
import java.nio.charset.Charset;
import java.security.NoSuchAlgorithmException;
+import java.text.ParseException;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.Date;
private ArrayList<LedgerTransactionAccount> accounts;
private String dataHash;
private boolean dataLoaded;
- public LedgerTransaction(Integer id, String dateString, String description) {
+ public LedgerTransaction(Integer id, String dateString, String description)
+ throws ParseException {
this(id, Globals.parseLedgerDate(dateString), description);
}
public LedgerTransaction(Integer id, Date date, String description) {
{
if (cTr.moveToFirst()) {
String dateString = cTr.getString(0);
- date = Globals.parseLedgerDate(dateString);
+ try {
+ date = Globals.parseLedgerDate(dateString);
+ }
+ catch (ParseException e) {
+ e.printStackTrace();
+ throw new RuntimeException(
+ String.format("Error parsing date '%s' from " + "transacion %d",
+ dateString, id));
+ }
description = cTr.getString(1);
try (Cursor cAcc = db.rawQuery("SELECT account_name, amount, currency FROM " +
/*
- * Copyright © 2018 Damyan Ivanov.
+ * Copyright © 2019 Damyan Ivanov.
* This file is part of Mobile-Ledger.
* Mobile-Ledger is free software: you can distribute it and/or modify it
* under the term of the GNU General Public License as published by
import java.util.regex.Pattern;
public class DatePickerFragment extends AppCompatDialogFragment
-implements DatePickerDialog.OnDateSetListener, DatePicker.OnDateChangedListener
-{
+ implements DatePickerDialog.OnDateSetListener, DatePicker.OnDateChangedListener {
+ static final Pattern reYMD = Pattern.compile("^\\s*(\\d+)\\d*/\\s*(\\d+)\\s*/\\s*(\\d+)\\s*$");
+ static final Pattern reMD = Pattern.compile("^\\s*(\\d+)\\s*/\\s*(\\d+)\\s*$");
+ static final Pattern reD = Pattern.compile("\\s*(\\d+)\\s*$");
@NonNull
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
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);
+ 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));
+ Matcher m = reYMD.matcher(present);
+ if (m.matches()) {
+ year = Integer.parseInt(m.group(1));
+ month = Integer.parseInt(m.group(2)) - 1; // month is 0-based
+ day = Integer.parseInt(m.group(3));
}
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));
+ m = reMD.matcher(present);
+ if (m.matches()) {
+ month = Integer.parseInt(m.group(1)) - 1;
+ day = Integer.parseInt(m.group(2));
+ }
+ else {
+ m = reD.matcher(present);
+ if (m.matches()) {
+ day = Integer.parseInt(m.group(1));
+ }
}
}
- DatePickerDialog dpd = new DatePickerDialog(Objects.requireNonNull(getActivity()), this, year, month, day);
+ 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();
@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);
+ 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));
+ if (c.get(GregorianCalendar.YEAR) == year) {
+ if (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));
}
+ else date.setText(String.format(Locale.US, "%d/%d/%d", year, month + 1, day));
TextView description = Objects.requireNonNull(getActivity())
.findViewById(R.id.new_transaction_description);
@Override
public void onDateChanged(DatePicker view, int year, int monthOfYear, int dayOfMonth) {
- TextView date = Objects.requireNonNull(getActivity()).findViewById(R.id.new_transaction_date);
+ 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));
+ if (c.get(GregorianCalendar.YEAR) == year) {
+ if (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));
+ }
}
+ else date.setText(String.format(Locale.US, "%d/%d/%d", year, monthOfYear + 1, dayOfMonth));
TextView description = Objects.requireNonNull(getActivity())
.findViewById(R.id.new_transaction_description);
import android.widget.LinearLayout;
import android.widget.ProgressBar;
import android.widget.TextView;
+import android.widget.Toast;
import net.ktnx.mobileledger.R;
import net.ktnx.mobileledger.async.RefreshDescriptionsTask;
if (retrieveTransactionsTask != null) retrieveTransactionsTask.cancel(false);
bTransactionListCancelDownload.setEnabled(false);
}
- public void onRetrieveDone(boolean success) {
+ public void onRetrieveDone(String error) {
progressLayout.setVisibility(View.GONE);
- updateLastUpdateTextFromDB();
- new RefreshDescriptionsTask().execute();
+ if (error == null) {
+ updateLastUpdateTextFromDB();
+
+ new RefreshDescriptionsTask().execute();
+ }
+ else Toast.makeText(this, error, Toast.LENGTH_LONG).show();
}
public void onRetrieveStart() {
progressBar.setIndeterminate(true);
import android.widget.TableLayout;
import android.widget.TableRow;
import android.widget.TextView;
+import android.widget.Toast;
import net.ktnx.mobileledger.R;
import net.ktnx.mobileledger.async.SaveTransactionTask;
import net.ktnx.mobileledger.utils.Globals;
import net.ktnx.mobileledger.utils.MLDB;
+import java.text.ParseException;
import java.util.Date;
import java.util.Objects;
if (mSave != null) mSave.setVisible(false);
toggleAllEditing(false);
progress.setVisibility(View.VISIBLE);
+ try {
- saver = new SaveTransactionTask(this);
+ saver = new SaveTransactionTask(this);
- String dateString = tvDate.getText().toString();
- Date date;
- if (dateString.isEmpty()) date = new Date();
- else date = Globals.parseLedgerDate(dateString);
- LedgerTransaction tr = new LedgerTransaction(date, tvDescription.getText().toString());
+ String dateString = tvDate.getText().toString();
+ Date date;
+ if (dateString.isEmpty()) date = new Date();
+ else date = Globals.parseLedgerDate(dateString);
+ LedgerTransaction tr = new LedgerTransaction(date, tvDescription.getText().toString());
- TableLayout table = findViewById(R.id.new_transaction_accounts_table);
- for (int i = 0; i < table.getChildCount(); i++) {
- TableRow row = (TableRow) table.getChildAt(i);
- String acc = ((TextView) row.getChildAt(0)).getText().toString();
- String amt = ((TextView) row.getChildAt(1)).getText().toString();
- LedgerTransactionAccount item =
- amt.length() > 0 ? new LedgerTransactionAccount(acc, Float.parseFloat(amt))
- : new LedgerTransactionAccount(acc);
+ TableLayout table = findViewById(R.id.new_transaction_accounts_table);
+ for (int i = 0; i < table.getChildCount(); i++) {
+ TableRow row = (TableRow) table.getChildAt(i);
+ String acc = ((TextView) row.getChildAt(0)).getText().toString();
+ String amt = ((TextView) row.getChildAt(1)).getText().toString();
+ LedgerTransactionAccount item =
+ amt.length() > 0 ? new LedgerTransactionAccount(acc, Float.parseFloat(amt))
+ : new LedgerTransactionAccount(acc);
+
+ tr.addAccount(item);
+ }
+ saver.execute(tr);
+ }
+ catch (ParseException e) {
+ Log.d("new-transaction", "Parse error", e);
+ Toast.makeText(this, getResources().getString(R.string.error_invalid_date),
+ Toast.LENGTH_LONG).show();
+ tvDate.requestFocus();
+
+ progress.setVisibility(View.GONE);
+ toggleAllEditing(true);
+ if (mSave != null) mSave.setVisible(true);
+ }
+ catch (Exception e) {
+ Log.d("new-transaction", "Unknown error", e);
- tr.addAccount(item);
+ progress.setVisibility(View.GONE);
+ toggleAllEditing(true);
+ if (mSave != null) mSave.setVisible(true);
}
- saver.execute(tr);
}
private void toggleAllEditing(boolean enabled) {
tvDate.setEnabled(enabled);
import android.view.ViewGroup;
import android.view.inputmethod.InputMethodManager;
import android.widget.AutoCompleteTextView;
+import android.widget.Toast;
import net.ktnx.mobileledger.R;
import net.ktnx.mobileledger.model.Data;
TransactionListViewModel.scheduleTransactionListReload();
TransactionListViewModel.updating.addObserver(
(o, arg) -> swiper.setRefreshing(TransactionListViewModel.updating.get()));
+ TransactionListViewModel.updateError.addObserver(new Observer() {
+ @Override
+ public void update(Observable o, Object arg) {
+ String err = TransactionListViewModel.updateError.get();
+ if (err == null) return;
+ Toast.makeText(mActivity, err, Toast.LENGTH_SHORT).show();
+ TransactionListViewModel.updateError.set(null);
+ }
+ });
Data.transactions.addObserver(
(o, arg) -> mActivity.runOnUiThread(() -> modelAdapter.notifyDataSetChanged()));
public class TransactionListViewModel extends ViewModel {
public static ObservableValue<Boolean> updating = new ObservableValue<>();
+ public static ObservableValue<String> updateError = new ObservableValue<>();
public static void scheduleTransactionListReload() {
String filter = TransactionListFragment.accountFilter.get();
- AsyncTask<String, Void, List<TransactionListItem>> task = new UTT();
+ AsyncTask<String, Void, String> task = new UTT();
task.execute(filter);
}
public static TransactionListItem getTransactionListItem(int position) {
}
private static class UTT extends UpdateTransactionsTask {
@Override
- protected void onPostExecute(List<TransactionListItem> list) {
- super.onPostExecute(list);
- if (list != null) Data.transactions.set(list);
+ protected void onPostExecute(String error) {
+ super.onPostExecute(error);
+ if (error != null) updateError.set(error);
}
}
}
import android.app.Activity;
import android.content.Context;
import android.support.annotation.ColorInt;
+import android.util.Log;
import android.view.View;
import android.view.inputmethod.InputMethodManager;
import java.text.ParseException;
import java.text.SimpleDateFormat;
+import java.util.Calendar;
import java.util.Date;
import java.util.Locale;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
public final class Globals {
@ColorInt
public static String[] monthNames;
private static SimpleDateFormat ledgerDateFormatter =
new SimpleDateFormat("yyyy/MM/dd", Locale.US);
+ private static Pattern reLedgerDate =
+ Pattern.compile("^(?:(\\d+)/)??(?:(\\d\\d?)/)?(\\d\\d?)$");
public static void hideSoftKeyboard(Activity act) {
// hide the keyboard
View v = act.getCurrentFocus();
imm.hideSoftInputFromWindow(v.getWindowToken(), 0);
}
}
- public static Date parseLedgerDate(String dateString) {
- try {
- return ledgerDateFormatter.parse(dateString);
- }
- catch (ParseException e) {
- throw new RuntimeException(String.format("Error parsing date '%s'", dateString), e);
+ public static Date parseLedgerDate(String dateString) throws ParseException {
+ Matcher m = reLedgerDate.matcher(dateString);
+ if (!m.matches()) throw new ParseException(dateString, 0);
+
+ String year = m.group(1);
+ String month = m.group(2);
+ String day = m.group(3);
+
+ String toParse;
+ if (year == null) {
+ Calendar now = Calendar.getInstance();
+ int thisYear = now.get(Calendar.YEAR);
+ if (month == null) {
+ int thisMonth = now.get(Calendar.MONTH) + 1;
+ toParse = String.format(Locale.US, "%04d/%02d/%s", thisYear, thisMonth, dateString);
+ }
+ else toParse = String.format(Locale.US, "%04d/%s", thisYear, dateString);
}
+ else toParse = dateString;
+
+ Log.d("globals", String.format("Parsing date '%s'", toParse));
+ return ledgerDateFormatter.parse(toParse);
}
public static String formatLedgerDate(Date date) {
return ledgerDateFormatter.format(date);
<string name="new_profile_title" type="id">New profile</string>
<string name="delete_profile">Delete profile</string>
<string name="delete">Delete</string>
+ <string name="error_invalid_date">Invalid date</string>
<string-array name="month_names">
<item>January</item>
<item>February</item>