import android.os.OperationCanceledException;
import android.util.Log;
+import net.ktnx.mobileledger.err.HTTPException;
import net.ktnx.mobileledger.json.AccountListParser;
import net.ktnx.mobileledger.json.ParsedBalance;
import net.ktnx.mobileledger.json.ParsedLedgerAccount;
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;
public class RetrieveTransactionsTask
extends AsyncTask<Void, RetrieveTransactionsTask.Progress, String> {
- private static final int MATCHING_TRANSACTIONS_LIMIT = 50;
+ private static final int MATCHING_TRANSACTIONS_LIMIT = 150;
private static final Pattern reComment = Pattern.compile("^\\s*;");
private static final Pattern reTransactionStart = Pattern.compile("<tr class=\"title\" " +
"id=\"transaction-(\\d+)\"><td class=\"date\"[^\"]*>([\\d.-]+)</td>");
context.onRetrieveDone(null);
}
private String retrieveTransactionListLegacy(MobileLedgerProfile profile)
- throws IOException, ParseException {
+ throws IOException, ParseException, HTTPException {
Progress progress = new Progress();
int maxTransactionId = Progress.INDETERMINATE;
ArrayList<LedgerAccount> accountList = new ArrayList<>();
HashMap<String, Void> accountNames = new HashMap<>();
- LedgerAccount lastAccount = null;
+ HashMap<String, LedgerAccount> syntheticAccounts = new HashMap<>();
+ LedgerAccount lastAccount = null, prevAccount = null;
boolean onlyStarred = Data.optShowOnlyStarred.get();
HttpURLConnection http = NetworkUtil.prepareConnection(profile, "journal");
http.setAllowUserInteraction(false);
publishProgress(progress);
- try (SQLiteDatabase db = MLDB.getWritableDatabase()) {
+ switch (http.getResponseCode()) {
+ case 200:
+ break;
+ default:
+ throw new HTTPException(http.getResponseCode(), http.getResponseMessage());
+ }
+ try (SQLiteDatabase db = MLDB.getDatabase()) {
try (InputStream resp = http.getInputStream()) {
if (http.getResponseCode() != 200)
throw new IOException(String.format("HTTP error %d", http.getResponseCode()));
if (line.equals("<h2>General Journal</h2>")) {
state = ParserState.EXPECTING_TRANSACTION;
L("→ expecting transaction");
- Data.accounts.set(accountList);
+ // commit the current transaction and start a new one
+ // the account list in the UI should reflect the (committed)
+ // state of the database
+ db.setTransactionSuccessful();
+ db.endTransaction();
+ Data.accounts.setList(accountList);
+ db.beginTransaction();
continue;
}
m = reAccountName.matcher(line);
acct_name = acct_name.replace("\"", "");
L(String.format("found account: %s", acct_name));
- lastAccount = profile.loadAccount(acct_name);
- if (lastAccount == null) {
+ prevAccount = lastAccount;
+ lastAccount = profile.tryLoadAccount(db, acct_name);
+ if (lastAccount == null)
lastAccount = new LedgerAccount(acct_name);
- profile.storeAccount(db, lastAccount);
- }
+ else lastAccount.removeAmounts();
+ profile.storeAccount(db, lastAccount);
+ if (prevAccount != null) prevAccount
+ .setHasSubAccounts(prevAccount.isParentOf(lastAccount));
// make sure the parent account(s) are present,
// synthesising them if necessary
+ // this happens when the (missing-in-HTML) parent account has
+ // only one child so we create a synthetic parent account record,
+ // copying the amounts when child's amounts are parsed
String parentName = lastAccount.getParentName();
if (parentName != null) {
Stack<String> toAppend = new Stack<>();
parentName =
new LedgerAccount(parentName).getParentName();
}
+ syntheticAccounts.clear();
while (!toAppend.isEmpty()) {
String aName = toAppend.pop();
- LedgerAccount acc = new LedgerAccount(aName);
- acc.setHidden(lastAccount.isHidden());
- if (!onlyStarred || !acc.isHidden())
- accountList.add(acc);
+ LedgerAccount acc = profile.tryLoadAccount(db, aName);
+ if (acc == null) {
+ acc = new LedgerAccount(aName);
+ acc.setHiddenByStar(lastAccount.isHiddenByStar());
+ acc.setExpanded(!lastAccount.hasSubAccounts() ||
+ lastAccount.isExpanded());
+ }
+ acc.setHasSubAccounts(true);
+ acc.removeAmounts(); // filled below when amounts are parsed
+ if ((!onlyStarred || !acc.isHiddenByStar()) &&
+ acc.isVisible(accountList)) accountList.add(acc);
L(String.format("gap-filling with %s", aName));
accountNames.put(aName, null);
profile.storeAccount(db, acc);
+ syntheticAccounts.put(aName, acc);
}
}
- if (!onlyStarred || !lastAccount.isHidden())
+ if ((!onlyStarred || !lastAccount.isHiddenByStar()) &&
+ lastAccount.isVisible(accountList))
accountList.add(lastAccount);
accountNames.put(acct_name, null);
if (currency == null) currency = "";
value = value.replace(',', '.');
L("curr=" + currency + ", value=" + value);
+ final float val = Float.parseFloat(value);
profile.storeAccountValue(db, lastAccount.getName(), currency,
- Float.valueOf(value));
- lastAccount.addAmount(Float.parseFloat(value), currency);
+ val);
+ lastAccount.addAmount(val, currency);
+ for (LedgerAccount syn : syntheticAccounts.values()) {
+ syn.addAmount(val, currency);
+ profile.storeAccountValue(db, syn.getName(), currency, val);
+ }
}
if (match_found) {
if (matchedTransactionsCount ==
MATCHING_TRANSACTIONS_LIMIT)
{
- profile.markTransactionsBeforeTransactionAsPresent(db, transaction);
+ profile.markTransactionsBeforeTransactionAsPresent(db,
+ transaction);
progress.setTotal(progress.getProgress());
publishProgress(progress);
break LINES;
new String[]{profile.getUuid()});
db.execSQL("update accounts set keep=0 where profile=?;", new String[]{profile.getUuid()});
}
- private boolean retrieveAccountList(MobileLedgerProfile profile) throws IOException {
+ private boolean retrieveAccountList(MobileLedgerProfile profile)
+ throws IOException, HTTPException {
Progress progress = new Progress();
HttpURLConnection http = NetworkUtil.prepareConnection(profile, "accounts");
http.setAllowUserInteraction(false);
+ switch (http.getResponseCode()) {
+ case 200:
+ break;
+ case 404:
+ return false;
+ default:
+ throw new HTTPException(http.getResponseCode(), http.getResponseMessage());
+ }
publishProgress(progress);
- try (SQLiteDatabase db = MLDB.getWritableDatabase()) {
- try (InputStream resp = http.getInputStream()) {
- if (http.getResponseCode() != 200)
- throw new IOException(String.format("HTTP error %d", http.getResponseCode()));
+ SQLiteDatabase db = MLDB.getDatabase();
+ ArrayList<LedgerAccount> accountList = new ArrayList<>();
+ boolean listFilledOK = false;
+ try (InputStream resp = http.getInputStream()) {
+ if (http.getResponseCode() != 200)
+ throw new IOException(String.format("HTTP error %d", http.getResponseCode()));
- db.beginTransaction();
- try {
- profile.markAccountsAsNotPresent(db);
+ db.beginTransaction();
+ try {
+ profile.markAccountsAsNotPresent(db);
- AccountListParser parser = new AccountListParser(resp);
- ArrayList<LedgerAccount> accountList = new ArrayList<>();
+ AccountListParser parser = new AccountListParser(resp);
- while (true) {
- throwIfCancelled();
- ParsedLedgerAccount parsedAccount = parser.nextAccount();
- if (parsedAccount == null) break;
-
- LedgerAccount acc = new LedgerAccount(parsedAccount.getAname());
- profile.storeAccount(db, acc);
- for (ParsedBalance b : parsedAccount.getAebalance()) {
- profile.storeAccountValue(db, acc.getName(), b.getAcommodity(),
- b.getAquantity().asFloat());
+ LedgerAccount prevAccount = null;
+
+ while (true) {
+ throwIfCancelled();
+ ParsedLedgerAccount parsedAccount = parser.nextAccount();
+ if (parsedAccount == null) break;
+
+ LedgerAccount acc = profile.tryLoadAccount(db, parsedAccount.getAname());
+ if (acc == null) acc = new LedgerAccount(parsedAccount.getAname());
+ else acc.removeAmounts();
+
+ profile.storeAccount(db, acc);
+ String lastCurrency = null;
+ float lastCurrencyAmount = 0;
+ for (ParsedBalance b : parsedAccount.getAibalance()) {
+ final String currency = b.getAcommodity();
+ final float amount = b.getAquantity().asFloat();
+ if (currency.equals(lastCurrency)) lastCurrencyAmount += amount;
+ else {
+ if (lastCurrency != null) {
+ profile.storeAccountValue(db, acc.getName(), lastCurrency,
+ lastCurrencyAmount);
+ acc.addAmount(lastCurrencyAmount, lastCurrency);
+ }
+ lastCurrency = currency;
+ lastCurrencyAmount = amount;
}
+ }
+ if (lastCurrency != null) {
+ profile.storeAccountValue(db, acc.getName(), lastCurrency,
+ lastCurrencyAmount);
+ acc.addAmount(lastCurrencyAmount, lastCurrency);
+ }
+
+ if (acc.isVisible(accountList)) accountList.add(acc);
- accountList.add(acc);
+ if (prevAccount != null) {
+ prevAccount.setHasSubAccounts(
+ acc.getName().startsWith(prevAccount.getName() + ":"));
}
- throwIfCancelled();
- profile.deleteNotPresentAccounts(db);
- throwIfCancelled();
- db.setTransactionSuccessful();
- Data.accounts.set(accountList);
- }
- finally {
- db.endTransaction();
+ prevAccount = acc;
}
+ throwIfCancelled();
+
+ profile.deleteNotPresentAccounts(db);
+ throwIfCancelled();
+ db.setTransactionSuccessful();
+ listFilledOK = true;
+ }
+ finally {
+ db.endTransaction();
}
}
+ // should not be set in the DB transaction, because of a possible deadlock
+ // with the main and DbOpQueueRunner threads
+ if (listFilledOK) Data.accounts.setList(accountList);
return true;
}
private boolean retrieveTransactionList(MobileLedgerProfile profile)
- throws IOException, ParseException {
+ throws IOException, ParseException, HTTPException {
Progress progress = new Progress();
int maxTransactionId = Progress.INDETERMINATE;
HttpURLConnection http = NetworkUtil.prepareConnection(profile, "transactions");
http.setAllowUserInteraction(false);
publishProgress(progress);
- try (SQLiteDatabase db = MLDB.getWritableDatabase()) {
- try (InputStream resp = http.getInputStream()) {
- if (http.getResponseCode() != 200)
- throw new IOException(String.format("HTTP error %d", http.getResponseCode()));
- throwIfCancelled();
- db.beginTransaction();
- try {
- profile.markTransactionsAsNotPresent(db);
+ switch (http.getResponseCode()) {
+ case 200:
+ break;
+ case 404:
+ return false;
+ default:
+ throw new HTTPException(http.getResponseCode(), http.getResponseMessage());
+ }
+ SQLiteDatabase db = MLDB.getDatabase();
+ try (InputStream resp = http.getInputStream()) {
+ if (http.getResponseCode() != 200)
+ throw new IOException(String.format("HTTP error %d", http.getResponseCode()));
+ throwIfCancelled();
+ db.beginTransaction();
+ try {
+ profile.markTransactionsAsNotPresent(db);
- int matchedTransactionsCount = 0;
- TransactionListParser parser = new TransactionListParser(resp);
+ int matchedTransactionsCount = 0;
+ TransactionListParser parser = new TransactionListParser(resp);
- int processedTransactionCount = 0;
+ int processedTransactionCount = 0;
- while (true) {
- throwIfCancelled();
- ParsedLedgerTransaction parsedTransaction = parser.nextTransaction();
- throwIfCancelled();
- if (parsedTransaction == null) break;
- LedgerTransaction transaction = parsedTransaction.asLedgerTransaction();
- if (transaction.existsInDb(db)) {
- profile.markTransactionAsPresent(db, transaction);
- matchedTransactionsCount++;
-
- if (matchedTransactionsCount == MATCHING_TRANSACTIONS_LIMIT) {
- profile.markTransactionsBeforeTransactionAsPresent(db, transaction);
- progress.setTotal(progress.getProgress());
- publishProgress(progress);
- db.setTransactionSuccessful();
- profile.setLastUpdateStamp();
- return true;
- }
+ DetectedTransactionOrder transactionOrder = DetectedTransactionOrder.UNKNOWN;
+ int orderAccumulator = 0;
+ int lastTransactionId = 0;
+
+ while (true) {
+ throwIfCancelled();
+ ParsedLedgerTransaction parsedTransaction = parser.nextTransaction();
+ throwIfCancelled();
+ if (parsedTransaction == null) break;
+
+ LedgerTransaction transaction = parsedTransaction.asLedgerTransaction();
+ if (transaction.getId() > lastTransactionId) orderAccumulator++;
+ else orderAccumulator--;
+ lastTransactionId = transaction.getId();
+ if (transactionOrder == DetectedTransactionOrder.UNKNOWN) {
+ if (orderAccumulator > 30) {
+ transactionOrder = DetectedTransactionOrder.FILE;
+ Log.d("rtt", String.format(
+ "Detected native file order after %d transactions (factor %d)",
+ processedTransactionCount, orderAccumulator));
+ progress.setTotal(Data.transactions.size());
}
- else {
- profile.storeTransaction(db, transaction);
- matchedTransactionsCount = 0;
- progress.setTotal(maxTransactionId);
+ else if (orderAccumulator < -30) {
+ transactionOrder = DetectedTransactionOrder.REVERSE_CHRONOLOGICAL;
+ Log.d("rtt", String.format(
+ "Detected reverse chronological order after %d transactions (factor %d)",
+ processedTransactionCount, orderAccumulator));
}
+ }
- progress.setProgress(++processedTransactionCount);
- publishProgress(progress);
+ if (transaction.existsInDb(db)) {
+ profile.markTransactionAsPresent(db, transaction);
+ matchedTransactionsCount++;
+
+ if ((transactionOrder == DetectedTransactionOrder.REVERSE_CHRONOLOGICAL) &&
+ (matchedTransactionsCount == MATCHING_TRANSACTIONS_LIMIT))
+ {
+ profile.markTransactionsBeforeTransactionAsPresent(db, transaction);
+ progress.setTotal(progress.getProgress());
+ publishProgress(progress);
+ db.setTransactionSuccessful();
+ profile.setLastUpdateStamp();
+ return true;
+ }
+ }
+ else {
+ profile.storeTransaction(db, transaction);
+ matchedTransactionsCount = 0;
+ progress.setTotal(maxTransactionId);
}
- throwIfCancelled();
- profile.deleteNotPresentTransactions(db);
- throwIfCancelled();
- db.setTransactionSuccessful();
- profile.setLastUpdateStamp();
- }
- finally {
- db.endTransaction();
+
+ if ((transactionOrder != DetectedTransactionOrder.UNKNOWN) &&
+ ((progress.getTotal() == Progress.INDETERMINATE) ||
+ (progress.getTotal() < transaction.getId())))
+ progress.setTotal(transaction.getId());
+
+ progress.setProgress(++processedTransactionCount);
+ publishProgress(progress);
}
+
+ throwIfCancelled();
+ profile.deleteNotPresentTransactions(db);
+ throwIfCancelled();
+ db.setTransactionSuccessful();
+ profile.setLastUpdateStamp();
+ }
+ finally {
+ db.endTransaction();
}
}
return true;
}
+
@SuppressLint("DefaultLocale")
@Override
protected String doInBackground(Void... params) {
e.printStackTrace();
return "Invalid server URL";
}
- catch (FileNotFoundException e) {
+ catch (HTTPException e) {
e.printStackTrace();
- return "Invalid user name or password";
+ return String.format("HTTP error %d: %s", e.getResponseCode(), e.getResponseMessage());
}
catch (IOException e) {
e.printStackTrace();
- return "Network error";
+ return e.getLocalizedMessage();
}
catch (ParseException e) {
e.printStackTrace();
private void throwIfCancelled() {
if (isCancelled()) throw new OperationCanceledException(null);
}
+ enum DetectedTransactionOrder {UNKNOWN, REVERSE_CHRONOLOGICAL, FILE}
private enum ParserState {
EXPECTING_ACCOUNT, EXPECTING_ACCOUNT_AMOUNT, EXPECTING_JOURNAL, EXPECTING_TRANSACTION,