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