]> git.ktnx.net Git - mobile-ledger.git/blob - app/src/main/java/net/ktnx/mobileledger/async/RetrieveTransactionsTask.java
legacy text parser: add support for currency before the amount
[mobile-ledger.git] / app / src / main / java / net / ktnx / mobileledger / async / RetrieveTransactionsTask.java
1 /*
2  * Copyright © 2019 Damyan Ivanov.
3  * This file is part of MoLe.
4  * MoLe is free software: you can distribute it and/or modify it
5  * under the term of the GNU General Public License as published by
6  * the Free Software Foundation, either version 3 of the License, or
7  * (at your opinion), any later version.
8  *
9  * MoLe is distributed in the hope that it will be useful,
10  * but WITHOUT ANY WARRANTY; without even the implied warranty of
11  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12  * GNU General Public License terms for details.
13  *
14  * You should have received a copy of the GNU General Public License
15  * along with MoLe. If not, see <https://www.gnu.org/licenses/>.
16  */
17
18 package net.ktnx.mobileledger.async;
19
20 import android.annotation.SuppressLint;
21 import android.database.sqlite.SQLiteDatabase;
22 import android.os.AsyncTask;
23 import android.os.OperationCanceledException;
24
25 import androidx.annotation.NonNull;
26
27 import net.ktnx.mobileledger.App;
28 import net.ktnx.mobileledger.err.HTTPException;
29 import net.ktnx.mobileledger.json.v1_15.AccountListParser;
30 import net.ktnx.mobileledger.json.v1_15.ParsedBalance;
31 import net.ktnx.mobileledger.json.v1_15.ParsedLedgerAccount;
32 import net.ktnx.mobileledger.json.v1_15.ParsedLedgerTransaction;
33 import net.ktnx.mobileledger.json.v1_15.TransactionListParser;
34 import net.ktnx.mobileledger.model.Data;
35 import net.ktnx.mobileledger.model.LedgerAccount;
36 import net.ktnx.mobileledger.model.LedgerTransaction;
37 import net.ktnx.mobileledger.model.LedgerTransactionAccount;
38 import net.ktnx.mobileledger.model.MobileLedgerProfile;
39 import net.ktnx.mobileledger.ui.activity.MainActivity;
40 import net.ktnx.mobileledger.utils.NetworkUtil;
41
42 import java.io.BufferedReader;
43 import java.io.IOException;
44 import java.io.InputStream;
45 import java.io.InputStreamReader;
46 import java.lang.ref.WeakReference;
47 import java.net.HttpURLConnection;
48 import java.net.MalformedURLException;
49 import java.net.URLDecoder;
50 import java.nio.charset.StandardCharsets;
51 import java.text.ParseException;
52 import java.util.ArrayList;
53 import java.util.HashMap;
54 import java.util.Locale;
55 import java.util.Stack;
56 import java.util.regex.Matcher;
57 import java.util.regex.Pattern;
58
59 import static net.ktnx.mobileledger.utils.Logger.debug;
60
61
62 public class RetrieveTransactionsTask
63         extends AsyncTask<Void, RetrieveTransactionsTask.Progress, String> {
64     private static final int MATCHING_TRANSACTIONS_LIMIT = 150;
65     private static final Pattern reComment = Pattern.compile("^\\s*;");
66     private static final Pattern reTransactionStart = Pattern.compile("<tr class=\"title\" " +
67                                                                       "id=\"transaction-(\\d+)\"><td class=\"date\"[^\"]*>([\\d.-]+)</td>");
68     private static final Pattern reTransactionDescription =
69             Pattern.compile("<tr class=\"posting\" title=\"(\\S+)\\s(.+)");
70     private static final Pattern reTransactionDetails = Pattern.compile(
71             "^\\s+" + "([!*]\\s+)?" + "(\\S[\\S\\s]+\\S)\\s\\s+" + "(?:([^\\d\\s+\\-]+)\\s*)?" +
72             "([-+]?\\d[\\d,.]*)" + "(?:\\s*([^\\d\\s+\\-]+)\\s*$)?");
73     private static final Pattern reEnd = Pattern.compile("\\bid=\"addmodal\"");
74     private static final Pattern reDecimalPoint = Pattern.compile("\\.\\d\\d?$");
75     private static final Pattern reDecimalComma = Pattern.compile(",\\d\\d?$");
76     private WeakReference<MainActivity> contextRef;
77     // %3A is '='
78     private Pattern reAccountName = Pattern.compile("/register\\?q=inacct%3A([a-zA-Z0-9%]+)\"");
79     private Pattern reAccountValue = Pattern.compile(
80             "<span class=\"[^\"]*\\bamount\\b[^\"]*\">\\s*([-+]?[\\d.,]+)(?:\\s+(\\S+))?</span>");
81     private MobileLedgerProfile profile;
82     public RetrieveTransactionsTask(WeakReference<MainActivity> contextRef,
83                                     @NonNull MobileLedgerProfile profile) {
84         this.contextRef = contextRef;
85         this.profile = profile;
86     }
87     private static void L(String msg) {
88         //debug("transaction-parser", msg);
89     }
90     static LedgerTransactionAccount parseTransactionAccountLine(String line) {
91         Matcher m = reTransactionDetails.matcher(line);
92         if (m.find()) {
93             String postingStatus = m.group(1);
94             String acc_name = m.group(2);
95             String currencyPre = m.group(3);
96             String amount = m.group(4);
97             String currencyPost = m.group(5);
98
99             String currency = null;
100             if ((currencyPre != null) && (currencyPre.length() > 0)) {
101                 if ((currencyPost != null) && (currencyPost.length() > 0))
102                     return null;
103                 currency = currencyPre;
104             }
105             else if ((currencyPost != null) && (currencyPost.length() > 0)) {
106                 currency = currencyPost;
107             }
108
109             amount = amount.replace(',', '.');
110
111             return new LedgerTransactionAccount(acc_name, Float.valueOf(amount), currency, null);
112         }
113         else {
114             return null;
115         }
116     }
117     @Override
118     protected void onProgressUpdate(Progress... values) {
119         super.onProgressUpdate(values);
120         MainActivity context = getContext();
121         if (context == null) return;
122         context.onRetrieveProgress(values[0]);
123     }
124     @Override
125     protected void onPreExecute() {
126         super.onPreExecute();
127         MainActivity context = getContext();
128         if (context == null) return;
129         context.onRetrieveStart();
130     }
131     @Override
132     protected void onPostExecute(String error) {
133         super.onPostExecute(error);
134         MainActivity context = getContext();
135         if (context == null) return;
136         context.onRetrieveDone(error);
137     }
138     @Override
139     protected void onCancelled() {
140         super.onCancelled();
141         MainActivity context = getContext();
142         if (context == null) return;
143         context.onRetrieveDone(null);
144     }
145     private String retrieveTransactionListLegacy()
146             throws IOException, ParseException, HTTPException {
147         Progress progress = new Progress();
148         int maxTransactionId = Progress.INDETERMINATE;
149         ArrayList<LedgerAccount> accountList = new ArrayList<>();
150         HashMap<String, Void> accountNames = new HashMap<>();
151         HashMap<String, LedgerAccount> syntheticAccounts = new HashMap<>();
152         LedgerAccount lastAccount = null, prevAccount = null;
153
154         HttpURLConnection http = NetworkUtil.prepareConnection(profile, "journal");
155         http.setAllowUserInteraction(false);
156         publishProgress(progress);
157         switch (http.getResponseCode()) {
158             case 200:
159                 break;
160             default:
161                 throw new HTTPException(http.getResponseCode(), http.getResponseMessage());
162         }
163
164         SQLiteDatabase db = App.getDatabase();
165         try (InputStream resp = http.getInputStream()) {
166             if (http.getResponseCode() != 200)
167                 throw new IOException(String.format("HTTP error %d", http.getResponseCode()));
168             db.beginTransaction();
169             try {
170                 prepareDbForRetrieval(db, profile);
171
172                 int matchedTransactionsCount = 0;
173
174
175                 ParserState state = ParserState.EXPECTING_ACCOUNT;
176                 String line;
177                 BufferedReader buf =
178                         new BufferedReader(new InputStreamReader(resp, StandardCharsets.UTF_8));
179
180                 int processedTransactionCount = 0;
181                 int transactionId = 0;
182                 LedgerTransaction transaction = null;
183                 LINES:
184                 while ((line = buf.readLine()) != null) {
185                     throwIfCancelled();
186                     Matcher m;
187                     m = reComment.matcher(line);
188                     if (m.find()) {
189                         // TODO: comments are ignored for now
190 //                            Log.v("transaction-parser", "Ignoring comment");
191                         continue;
192                     }
193                     //L(String.format("State is %d", updating));
194                     switch (state) {
195                         case EXPECTING_ACCOUNT:
196                             if (line.equals("<h2>General Journal</h2>")) {
197                                 state = ParserState.EXPECTING_TRANSACTION;
198                                 L("→ expecting transaction");
199                                 // commit the current transaction and start a new one
200                                 // the account list in the UI should reflect the (committed)
201                                 // state of the database
202                                 db.setTransactionSuccessful();
203                                 db.endTransaction();
204                                 Data.accounts.setList(accountList);
205                                 db.beginTransaction();
206                                 continue;
207                             }
208                             m = reAccountName.matcher(line);
209                             if (m.find()) {
210                                 String acct_encoded = m.group(1);
211                                 String acct_name = URLDecoder.decode(acct_encoded, "UTF-8");
212                                 acct_name = acct_name.replace("\"", "");
213                                 L(String.format("found account: %s", acct_name));
214
215                                 prevAccount = lastAccount;
216                                 lastAccount = profile.tryLoadAccount(db, acct_name);
217                                 if (lastAccount == null) lastAccount = new LedgerAccount(acct_name);
218                                 else lastAccount.removeAmounts();
219                                 profile.storeAccount(db, lastAccount);
220
221                                 if (prevAccount != null) prevAccount
222                                         .setHasSubAccounts(prevAccount.isParentOf(lastAccount));
223                                 // make sure the parent account(s) are present,
224                                 // synthesising them if necessary
225                                 // this happens when the (missing-in-HTML) parent account has
226                                 // only one child so we create a synthetic parent account record,
227                                 // copying the amounts when child's amounts are parsed
228                                 String parentName = lastAccount.getParentName();
229                                 if (parentName != null) {
230                                     Stack<String> toAppend = new Stack<>();
231                                     while (parentName != null) {
232                                         if (accountNames.containsKey(parentName)) break;
233                                         toAppend.push(parentName);
234                                         parentName = new LedgerAccount(parentName).getParentName();
235                                     }
236                                     syntheticAccounts.clear();
237                                     while (!toAppend.isEmpty()) {
238                                         String aName = toAppend.pop();
239                                         LedgerAccount acc = profile.tryLoadAccount(db, aName);
240                                         if (acc == null) {
241                                             acc = new LedgerAccount(aName);
242                                             acc.setHiddenByStar(lastAccount.isHiddenByStar());
243                                             acc.setExpanded(!lastAccount.hasSubAccounts() ||
244                                                             lastAccount.isExpanded());
245                                         }
246                                         acc.setHasSubAccounts(true);
247                                         acc.removeAmounts();    // filled below when amounts are parsed
248                                         if (acc.isVisible(accountList))
249                                             accountList.add(acc);
250                                         L(String.format("gap-filling with %s", aName));
251                                         accountNames.put(aName, null);
252                                         profile.storeAccount(db, acc);
253                                         syntheticAccounts.put(aName, acc);
254                                     }
255                                 }
256
257                                 if (lastAccount.isVisible(accountList))
258                                     accountList.add(lastAccount);
259                                 accountNames.put(acct_name, null);
260
261                                 state = ParserState.EXPECTING_ACCOUNT_AMOUNT;
262                                 L("→ expecting account amount");
263                             }
264                             break;
265
266                         case EXPECTING_ACCOUNT_AMOUNT:
267                             m = reAccountValue.matcher(line);
268                             boolean match_found = false;
269                             while (m.find()) {
270                                 throwIfCancelled();
271
272                                 match_found = true;
273                                 String value = m.group(1);
274                                 String currency = m.group(2);
275                                 if (currency == null) currency = "";
276
277                                 {
278                                     Matcher tmpM = reDecimalComma.matcher(value);
279                                     if (tmpM.find()) {
280                                         value = value.replace(".", "");
281                                         value = value.replace(',', '.');
282                                     }
283
284                                     tmpM = reDecimalPoint.matcher(value);
285                                     if (tmpM.find()) {
286                                         value = value.replace(",", "");
287                                         value = value.replace(" ", "");
288                                     }
289                                 }
290                                 L("curr=" + currency + ", value=" + value);
291                                 final float val = Float.parseFloat(value);
292                                 profile.storeAccountValue(db, lastAccount.getName(), currency, val);
293                                 lastAccount.addAmount(val, currency);
294                                 for (LedgerAccount syn : syntheticAccounts.values()) {
295                                     L(String.format(Locale.ENGLISH, "propagating %s %1.2f to %s",
296                                             currency, val, syn.getName()));
297                                     syn.addAmount(val, currency);
298                                     profile.storeAccountValue(db, syn.getName(), currency, val);
299                                 }
300                             }
301
302                             if (match_found) {
303                                 syntheticAccounts.clear();
304                                 state = ParserState.EXPECTING_ACCOUNT;
305                                 L("→ expecting account");
306                             }
307
308                             break;
309
310                         case EXPECTING_TRANSACTION:
311                             if (!line.isEmpty() && (line.charAt(0) == ' ')) continue;
312                             m = reTransactionStart.matcher(line);
313                             if (m.find()) {
314                                 transactionId = Integer.valueOf(m.group(1));
315                                 state = ParserState.EXPECTING_TRANSACTION_DESCRIPTION;
316                                 L(String.format(Locale.ENGLISH,
317                                         "found transaction %d → expecting description",
318                                         transactionId));
319                                 progress.setProgress(++processedTransactionCount);
320                                 if (maxTransactionId < transactionId)
321                                     maxTransactionId = transactionId;
322                                 if ((progress.getTotal() == Progress.INDETERMINATE) ||
323                                     (progress.getTotal() < transactionId))
324                                     progress.setTotal(transactionId);
325                                 publishProgress(progress);
326                             }
327                             m = reEnd.matcher(line);
328                             if (m.find()) {
329                                 L("--- transaction value complete ---");
330                                 break LINES;
331                             }
332                             break;
333
334                         case EXPECTING_TRANSACTION_DESCRIPTION:
335                             if (!line.isEmpty() && (line.charAt(0) == ' ')) continue;
336                             m = reTransactionDescription.matcher(line);
337                             if (m.find()) {
338                                 if (transactionId == 0) throw new TransactionParserException(
339                                         "Transaction Id is 0 while expecting " + "description");
340
341                                 String date = m.group(1);
342                                 try {
343                                     int equalsIndex = date.indexOf('=');
344                                     if (equalsIndex >= 0) date = date.substring(equalsIndex + 1);
345                                     transaction =
346                                             new LedgerTransaction(transactionId, date, m.group(2));
347                                 }
348                                 catch (ParseException e) {
349                                     e.printStackTrace();
350                                     return String.format("Error parsing date '%s'", date);
351                                 }
352                                 state = ParserState.EXPECTING_TRANSACTION_DETAILS;
353                                 L(String.format(Locale.ENGLISH,
354                                         "transaction %d created for %s (%s) →" +
355                                         " expecting details", transactionId, date, m.group(2)));
356                             }
357                             break;
358
359                         case EXPECTING_TRANSACTION_DETAILS:
360                             if (line.isEmpty()) {
361                                 // transaction data collected
362                                 if (transaction.existsInDb(db)) {
363                                     profile.markTransactionAsPresent(db, transaction);
364                                     matchedTransactionsCount++;
365
366                                     if (matchedTransactionsCount == MATCHING_TRANSACTIONS_LIMIT) {
367                                         profile.markTransactionsBeforeTransactionAsPresent(db,
368                                                 transaction);
369                                         progress.setTotal(progress.getProgress());
370                                         publishProgress(progress);
371                                         break LINES;
372                                     }
373                                 }
374                                 else {
375                                     profile.storeTransaction(db, transaction);
376                                     matchedTransactionsCount = 0;
377                                     progress.setTotal(maxTransactionId);
378                                 }
379
380                                 state = ParserState.EXPECTING_TRANSACTION;
381                                 L(String.format("transaction %s saved → expecting transaction",
382                                         transaction.getId()));
383                                 transaction.finishLoading();
384
385 // sounds like a good idea, but transaction-1 may not be the first one chronologically
386 // for example, when you add the initial seeding transaction after entering some others
387 //                                            if (transactionId == 1) {
388 //                                                L("This was the initial transaction. Terminating " +
389 //                                                  "parser");
390 //                                                break LINES;
391 //                                            }
392                             }
393                             else {
394                                 LedgerTransactionAccount lta = parseTransactionAccountLine(line);
395                                 if (lta != null) {
396                                     transaction.addAccount(lta);
397                                     L(String.format(Locale.ENGLISH, "%d: %s = %s",
398                                             transaction.getId(), lta.getAccountName(),
399                                             lta.getAmount()));
400                                 }
401                                 else throw new IllegalStateException(
402                                         String.format("Can't parse transaction %d " + "details: %s",
403                                                 transactionId, line));
404                             }
405                             break;
406                         default:
407                             throw new RuntimeException(
408                                     String.format("Unknown parser updating %s", state.name()));
409                     }
410                 }
411
412                 throwIfCancelled();
413
414                 profile.deleteNotPresentTransactions(db);
415                 db.setTransactionSuccessful();
416
417                 profile.setLastUpdateStamp();
418
419                 return null;
420             }
421             finally {
422                 db.endTransaction();
423             }
424         }
425     }
426     private void prepareDbForRetrieval(SQLiteDatabase db, MobileLedgerProfile profile) {
427         db.execSQL("UPDATE transactions set keep=0 where profile=?",
428                 new String[]{profile.getUuid()});
429         db.execSQL("update account_values set keep=0 where profile=?;",
430                 new String[]{profile.getUuid()});
431         db.execSQL("update accounts set keep=0 where profile=?;", new String[]{profile.getUuid()});
432     }
433     private boolean retrieveAccountList() throws IOException, HTTPException {
434         Progress progress = new Progress();
435
436         HttpURLConnection http = NetworkUtil.prepareConnection(profile, "accounts");
437         http.setAllowUserInteraction(false);
438         switch (http.getResponseCode()) {
439             case 200:
440                 break;
441             case 404:
442                 return false;
443             default:
444                 throw new HTTPException(http.getResponseCode(), http.getResponseMessage());
445         }
446         publishProgress(progress);
447         SQLiteDatabase db = App.getDatabase();
448         ArrayList<LedgerAccount> accountList = new ArrayList<>();
449         boolean listFilledOK = false;
450         try (InputStream resp = http.getInputStream()) {
451             if (http.getResponseCode() != 200)
452                 throw new IOException(String.format("HTTP error %d", http.getResponseCode()));
453
454             db.beginTransaction();
455             try {
456                 profile.markAccountsAsNotPresent(db);
457
458                 AccountListParser parser = new AccountListParser(resp);
459
460                 LedgerAccount prevAccount = null;
461
462                 while (true) {
463                     throwIfCancelled();
464                     ParsedLedgerAccount parsedAccount = parser.nextAccount();
465                     if (parsedAccount == null) break;
466
467                     LedgerAccount acc = profile.tryLoadAccount(db, parsedAccount.getAname());
468                     if (acc == null) acc = new LedgerAccount(parsedAccount.getAname());
469                     else acc.removeAmounts();
470
471                     profile.storeAccount(db, acc);
472                     String lastCurrency = null;
473                     float lastCurrencyAmount = 0;
474                     for (ParsedBalance b : parsedAccount.getAibalance()) {
475                         final String currency = b.getAcommodity();
476                         final float amount = b.getAquantity().asFloat();
477                         if (currency.equals(lastCurrency)) lastCurrencyAmount += amount;
478                         else {
479                             if (lastCurrency != null) {
480                                 profile.storeAccountValue(db, acc.getName(), lastCurrency,
481                                         lastCurrencyAmount);
482                                 acc.addAmount(lastCurrencyAmount, lastCurrency);
483                             }
484                             lastCurrency = currency;
485                             lastCurrencyAmount = amount;
486                         }
487                     }
488                     if (lastCurrency != null) {
489                         profile.storeAccountValue(db, acc.getName(), lastCurrency,
490                                 lastCurrencyAmount);
491                         acc.addAmount(lastCurrencyAmount, lastCurrency);
492                     }
493
494                     if (acc.isVisible(accountList)) accountList.add(acc);
495
496                     if (prevAccount != null) {
497                         prevAccount.setHasSubAccounts(
498                                 acc.getName().startsWith(prevAccount.getName() + ":"));
499                     }
500
501                     prevAccount = acc;
502                 }
503                 throwIfCancelled();
504
505                 profile.deleteNotPresentAccounts(db);
506                 throwIfCancelled();
507                 db.setTransactionSuccessful();
508                 listFilledOK = true;
509             }
510             finally {
511                 db.endTransaction();
512             }
513         }
514         // should not be set in the DB transaction, because of a possible deadlock
515         // with the main and DbOpQueueRunner threads
516         if (listFilledOK) Data.accounts.setList(accountList);
517
518         return true;
519     }
520     private boolean retrieveTransactionList() throws IOException, ParseException, HTTPException {
521         Progress progress = new Progress();
522         int maxTransactionId = Progress.INDETERMINATE;
523
524         HttpURLConnection http = NetworkUtil.prepareConnection(profile, "transactions");
525         http.setAllowUserInteraction(false);
526         publishProgress(progress);
527         switch (http.getResponseCode()) {
528             case 200:
529                 break;
530             case 404:
531                 return false;
532             default:
533                 throw new HTTPException(http.getResponseCode(), http.getResponseMessage());
534         }
535         SQLiteDatabase db = App.getDatabase();
536         try (InputStream resp = http.getInputStream()) {
537             if (http.getResponseCode() != 200)
538                 throw new IOException(String.format("HTTP error %d", http.getResponseCode()));
539             throwIfCancelled();
540             db.beginTransaction();
541             try {
542                 profile.markTransactionsAsNotPresent(db);
543
544                 int matchedTransactionsCount = 0;
545                 TransactionListParser parser = new TransactionListParser(resp);
546
547                 int processedTransactionCount = 0;
548
549                 DetectedTransactionOrder transactionOrder = DetectedTransactionOrder.UNKNOWN;
550                 int orderAccumulator = 0;
551                 int lastTransactionId = 0;
552
553                 while (true) {
554                     throwIfCancelled();
555                     ParsedLedgerTransaction parsedTransaction = parser.nextTransaction();
556                     throwIfCancelled();
557                     if (parsedTransaction == null) break;
558
559                     LedgerTransaction transaction = parsedTransaction.asLedgerTransaction();
560                     if (transaction.getId() > lastTransactionId) orderAccumulator++;
561                     else orderAccumulator--;
562                     lastTransactionId = transaction.getId();
563                     if (transactionOrder == DetectedTransactionOrder.UNKNOWN) {
564                         if (orderAccumulator > 30) {
565                             transactionOrder = DetectedTransactionOrder.FILE;
566                             debug("rtt", String.format(Locale.ENGLISH,
567                                     "Detected native file order after %d transactions (factor %d)",
568                                     processedTransactionCount, orderAccumulator));
569                             progress.setTotal(Data.transactions.size());
570                         }
571                         else if (orderAccumulator < -30) {
572                             transactionOrder = DetectedTransactionOrder.REVERSE_CHRONOLOGICAL;
573                             debug("rtt", String.format(Locale.ENGLISH,
574                                     "Detected reverse chronological order after %d transactions (factor %d)",
575                                     processedTransactionCount, orderAccumulator));
576                         }
577                     }
578
579                     if (transaction.existsInDb(db)) {
580                         profile.markTransactionAsPresent(db, transaction);
581                         matchedTransactionsCount++;
582
583                         if ((transactionOrder == DetectedTransactionOrder.REVERSE_CHRONOLOGICAL) &&
584                             (matchedTransactionsCount == MATCHING_TRANSACTIONS_LIMIT))
585                         {
586                             profile.markTransactionsBeforeTransactionAsPresent(db, transaction);
587                             progress.setTotal(progress.getProgress());
588                             publishProgress(progress);
589                             db.setTransactionSuccessful();
590                             profile.setLastUpdateStamp();
591                             return true;
592                         }
593                     }
594                     else {
595                         profile.storeTransaction(db, transaction);
596                         matchedTransactionsCount = 0;
597                         progress.setTotal(maxTransactionId);
598                     }
599
600
601                     if ((transactionOrder != DetectedTransactionOrder.UNKNOWN) &&
602                         ((progress.getTotal() == Progress.INDETERMINATE) ||
603                          (progress.getTotal() < transaction.getId())))
604                         progress.setTotal(transaction.getId());
605
606                     progress.setProgress(++processedTransactionCount);
607                     publishProgress(progress);
608                 }
609
610                 throwIfCancelled();
611                 profile.deleteNotPresentTransactions(db);
612                 throwIfCancelled();
613                 db.setTransactionSuccessful();
614                 profile.setLastUpdateStamp();
615             }
616             finally {
617                 db.endTransaction();
618             }
619         }
620
621         return true;
622     }
623
624     @SuppressLint("DefaultLocale")
625     @Override
626     protected String doInBackground(Void... params) {
627         Data.backgroundTaskStarted();
628         try {
629             if (!retrieveAccountList() || !retrieveTransactionList())
630                 return retrieveTransactionListLegacy();
631             return null;
632         }
633         catch (MalformedURLException e) {
634             e.printStackTrace();
635             return "Invalid server URL";
636         }
637         catch (HTTPException e) {
638             e.printStackTrace();
639             return String.format("HTTP error %d: %s", e.getResponseCode(), e.getResponseMessage());
640         }
641         catch (IOException e) {
642             e.printStackTrace();
643             return e.getLocalizedMessage();
644         }
645         catch (ParseException e) {
646             e.printStackTrace();
647             return "Network error";
648         }
649         catch (OperationCanceledException e) {
650             e.printStackTrace();
651             return "Operation cancelled";
652         }
653         finally {
654             Data.backgroundTaskFinished();
655         }
656     }
657     private MainActivity getContext() {
658         return contextRef.get();
659     }
660     private void throwIfCancelled() {
661         if (isCancelled()) throw new OperationCanceledException(null);
662     }
663     enum DetectedTransactionOrder {UNKNOWN, REVERSE_CHRONOLOGICAL, FILE}
664
665     private enum ParserState {
666         EXPECTING_ACCOUNT, EXPECTING_ACCOUNT_AMOUNT, EXPECTING_JOURNAL, EXPECTING_TRANSACTION,
667         EXPECTING_TRANSACTION_DESCRIPTION, EXPECTING_TRANSACTION_DETAILS
668     }
669
670     public class Progress {
671         public static final int INDETERMINATE = -1;
672         private int progress;
673         private int total;
674         Progress() {
675             this(INDETERMINATE, INDETERMINATE);
676         }
677         Progress(int progress, int total) {
678             this.progress = progress;
679             this.total = total;
680         }
681         public int getProgress() {
682             return progress;
683         }
684         protected void setProgress(int progress) {
685             this.progress = progress;
686         }
687         public int getTotal() {
688             return total;
689         }
690         protected void setTotal(int total) {
691             this.total = total;
692         }
693     }
694
695     private class TransactionParserException extends IllegalStateException {
696         TransactionParserException(String message) {
697             super(message);
698         }
699     }
700 }