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