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