From ea17ca9e3b03ca0090be03fdc0abbbbfd954be89 Mon Sep 17 00:00:00 2001 From: Damyan Ivanov Date: Sat, 12 Jan 2019 18:59:00 +0000 Subject: [PATCH] handle date parsing errors, handle real=fake dates --- .../async/RetrieveTransactionsTask.java | 48 +++++++------ .../async/UpdateTransactionsTask.java | 11 +-- .../mobileledger/model/LedgerTransaction.java | 14 +++- .../mobileledger/ui/DatePickerFragment.java | 68 +++++++++++-------- .../ui/activity/MainActivity.java | 11 ++- .../ui/activity/NewTransactionActivity.java | 53 ++++++++++----- .../TransactionListFragment.java | 10 +++ .../TransactionListViewModel.java | 9 +-- .../net/ktnx/mobileledger/utils/Globals.java | 33 +++++++-- app/src/main/res/values/strings.xml | 1 + 10 files changed, 175 insertions(+), 83 deletions(-) diff --git a/app/src/main/java/net/ktnx/mobileledger/async/RetrieveTransactionsTask.java b/app/src/main/java/net/ktnx/mobileledger/async/RetrieveTransactionsTask.java index 4da02e82..56a26621 100644 --- a/app/src/main/java/net/ktnx/mobileledger/async/RetrieveTransactionsTask.java +++ b/app/src/main/java/net/ktnx/mobileledger/async/RetrieveTransactionsTask.java @@ -23,7 +23,6 @@ import android.os.AsyncTask; import android.os.OperationCanceledException; import android.util.Log; -import net.ktnx.mobileledger.R; import net.ktnx.mobileledger.model.Data; import net.ktnx.mobileledger.model.LedgerAccount; import net.ktnx.mobileledger.model.LedgerTransaction; @@ -43,6 +42,7 @@ import java.lang.ref.WeakReference; 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; @@ -52,7 +52,7 @@ import java.util.regex.Pattern; public class RetrieveTransactionsTask - extends AsyncTask { + extends AsyncTask { private static final int MATCHING_TRANSACTIONS_LIMIT = 50; private static final Pattern reComment = Pattern.compile("^\\s*;"); private static final Pattern reTransactionStart = Pattern.compile(" 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( "\\s*([-+]?[\\d.,]+)(?:\\s+(\\S+))?"); @@ -90,26 +89,25 @@ public class RetrieveTransactionsTask 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 accountList = new ArrayList<>(); HashMap accountNames = new HashMap<>(); LedgerAccount lastAccount = null; @@ -248,7 +246,6 @@ public class RetrieveTransactionsTask m = reEnd.matcher(line); if (m.find()) { L("--- transaction value complete ---"); - success = true; break LINES; } break; @@ -262,13 +259,22 @@ public class RetrieveTransactionsTask "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; @@ -291,7 +297,6 @@ public class RetrieveTransactionsTask new Object[]{profile.getUuid(), transaction.getId() }); - success = true; progress.setTotal(progress.getProgress()); publishProgress(progress); break LINES; @@ -354,6 +359,8 @@ public class RetrieveTransactionsTask profile.setLongOption(MLDB.OPT_LAST_SCRAPE, now.getTime()); Data.lastUpdateDate.set(now); TransactionListViewModel.scheduleTransactionListReload(); + + return null; } finally { db.endTransaction(); @@ -362,25 +369,24 @@ public class RetrieveTransactionsTask } } 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(); diff --git a/app/src/main/java/net/ktnx/mobileledger/async/UpdateTransactionsTask.java b/app/src/main/java/net/ktnx/mobileledger/async/UpdateTransactionsTask.java index f58d3e5c..dc02fe9f 100644 --- a/app/src/main/java/net/ktnx/mobileledger/async/UpdateTransactionsTask.java +++ b/app/src/main/java/net/ktnx/mobileledger/async/UpdateTransactionsTask.java @@ -28,12 +28,12 @@ import net.ktnx.mobileledger.model.TransactionListItem; 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> { - protected List doInBackground(String[] filterAccName) { +public class UpdateTransactionsTask extends AsyncTask { + protected String doInBackground(String[] filterAccName) { Data.backgroundTaskCount.incrementAndGet(); String profile_uuid = Data.profile.get().getUuid(); try { @@ -88,7 +88,10 @@ public class UpdateTransactionsTask extends AsyncTask 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) { @@ -139,7 +141,15 @@ public class LedgerTransaction { { 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 " + diff --git a/app/src/main/java/net/ktnx/mobileledger/ui/DatePickerFragment.java b/app/src/main/java/net/ktnx/mobileledger/ui/DatePickerFragment.java index f801bdaa..d747f344 100644 --- a/app/src/main/java/net/ktnx/mobileledger/ui/DatePickerFragment.java +++ b/app/src/main/java/net/ktnx/mobileledger/ui/DatePickerFragment.java @@ -1,5 +1,5 @@ /* - * 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 @@ -37,8 +37,10 @@ import java.util.regex.Matcher; 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) { @@ -46,26 +48,33 @@ implements DatePickerDialog.OnDateSetListener, DatePicker.OnDateChangedListener 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(); @@ -77,15 +86,16 @@ implements DatePickerDialog.OnDateSetListener, DatePicker.OnDateChangedListener @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); @@ -94,15 +104,19 @@ implements DatePickerDialog.OnDateSetListener, DatePicker.OnDateChangedListener @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); diff --git a/app/src/main/java/net/ktnx/mobileledger/ui/activity/MainActivity.java b/app/src/main/java/net/ktnx/mobileledger/ui/activity/MainActivity.java index e079399e..37e6fd23 100644 --- a/app/src/main/java/net/ktnx/mobileledger/ui/activity/MainActivity.java +++ b/app/src/main/java/net/ktnx/mobileledger/ui/activity/MainActivity.java @@ -37,6 +37,7 @@ import android.view.View; 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; @@ -333,11 +334,15 @@ public class MainActivity extends AppCompatActivity { 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); diff --git a/app/src/main/java/net/ktnx/mobileledger/ui/activity/NewTransactionActivity.java b/app/src/main/java/net/ktnx/mobileledger/ui/activity/NewTransactionActivity.java index a08f88c1..13fc0620 100644 --- a/app/src/main/java/net/ktnx/mobileledger/ui/activity/NewTransactionActivity.java +++ b/app/src/main/java/net/ktnx/mobileledger/ui/activity/NewTransactionActivity.java @@ -41,6 +41,7 @@ import android.widget.ProgressBar; 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; @@ -52,6 +53,7 @@ import net.ktnx.mobileledger.ui.OnSwipeTouchListener; import net.ktnx.mobileledger.utils.Globals; import net.ktnx.mobileledger.utils.MLDB; +import java.text.ParseException; import java.util.Date; import java.util.Objects; @@ -133,27 +135,46 @@ public class NewTransactionActivity extends AppCompatActivity implements TaskCal 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); diff --git a/app/src/main/java/net/ktnx/mobileledger/ui/transaction_list/TransactionListFragment.java b/app/src/main/java/net/ktnx/mobileledger/ui/transaction_list/TransactionListFragment.java index f748ff1d..f77580ba 100644 --- a/app/src/main/java/net/ktnx/mobileledger/ui/transaction_list/TransactionListFragment.java +++ b/app/src/main/java/net/ktnx/mobileledger/ui/transaction_list/TransactionListFragment.java @@ -35,6 +35,7 @@ import android.view.View; 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; @@ -183,7 +184,16 @@ public class TransactionListFragment extends MobileLedgerListFragment { 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())); diff --git a/app/src/main/java/net/ktnx/mobileledger/ui/transaction_list/TransactionListViewModel.java b/app/src/main/java/net/ktnx/mobileledger/ui/transaction_list/TransactionListViewModel.java index 53f48d34..0cf97bca 100644 --- a/app/src/main/java/net/ktnx/mobileledger/ui/transaction_list/TransactionListViewModel.java +++ b/app/src/main/java/net/ktnx/mobileledger/ui/transaction_list/TransactionListViewModel.java @@ -29,10 +29,11 @@ import java.util.List; public class TransactionListViewModel extends ViewModel { public static ObservableValue updating = new ObservableValue<>(); + public static ObservableValue updateError = new ObservableValue<>(); public static void scheduleTransactionListReload() { String filter = TransactionListFragment.accountFilter.get(); - AsyncTask> task = new UTT(); + AsyncTask task = new UTT(); task.execute(filter); } public static TransactionListItem getTransactionListItem(int position) { @@ -47,9 +48,9 @@ public class TransactionListViewModel extends ViewModel { } private static class UTT extends UpdateTransactionsTask { @Override - protected void onPostExecute(List 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); } } } diff --git a/app/src/main/java/net/ktnx/mobileledger/utils/Globals.java b/app/src/main/java/net/ktnx/mobileledger/utils/Globals.java index cd3f8741..87730ca3 100644 --- a/app/src/main/java/net/ktnx/mobileledger/utils/Globals.java +++ b/app/src/main/java/net/ktnx/mobileledger/utils/Globals.java @@ -20,13 +20,17 @@ package net.ktnx.mobileledger.utils; 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 @@ -38,6 +42,8 @@ public final class Globals { 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(); @@ -47,13 +53,28 @@ public final class Globals { 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); diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 2390373d..74b54843 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -108,6 +108,7 @@ New profile Delete profile Delete + Invalid date January February -- 2.39.2