]> git.ktnx.net Git - mobile-ledger.git/blob - app/src/main/java/net/ktnx/mobileledger/async/RetrieveTransactionsTask.java
store a weak reference to the profile in the account object
[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                                 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(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                 listFilledOK = true;
531             }
532             finally {
533                 db.endTransaction();
534             }
535         }
536         // should not be set in the DB transaction, because of a possible deadlock
537         // with the main and DbOpQueueRunner threads
538         if (listFilledOK)
539             Data.accounts.setList(accountList);
540
541         return true;
542     }
543     private boolean retrieveTransactionList() throws IOException, ParseException, HTTPException {
544         Progress progress = new Progress();
545         int maxTransactionId = Progress.INDETERMINATE;
546
547         HttpURLConnection http = NetworkUtil.prepareConnection(profile, "transactions");
548         http.setAllowUserInteraction(false);
549         publishProgress(progress);
550         switch (http.getResponseCode()) {
551             case 200:
552                 break;
553             case 404:
554                 return false;
555             default:
556                 throw new HTTPException(http.getResponseCode(), http.getResponseMessage());
557         }
558         SQLiteDatabase db = App.getDatabase();
559         try (InputStream resp = http.getInputStream()) {
560             if (http.getResponseCode() != 200)
561                 throw new IOException(String.format("HTTP error %d", http.getResponseCode()));
562             throwIfCancelled();
563             db.beginTransaction();
564             try {
565                 profile.markTransactionsAsNotPresent(db);
566
567                 int matchedTransactionsCount = 0;
568                 TransactionListParser parser = new TransactionListParser(resp);
569
570                 int processedTransactionCount = 0;
571
572                 DetectedTransactionOrder transactionOrder = DetectedTransactionOrder.UNKNOWN;
573                 int orderAccumulator = 0;
574                 int lastTransactionId = 0;
575
576                 while (true) {
577                     throwIfCancelled();
578                     ParsedLedgerTransaction parsedTransaction = parser.nextTransaction();
579                     throwIfCancelled();
580                     if (parsedTransaction == null)
581                         break;
582
583                     LedgerTransaction transaction = parsedTransaction.asLedgerTransaction();
584                     if (transaction.getId() > lastTransactionId)
585                         orderAccumulator++;
586                     else
587                         orderAccumulator--;
588                     lastTransactionId = transaction.getId();
589                     if (transactionOrder == DetectedTransactionOrder.UNKNOWN) {
590                         if (orderAccumulator > 30) {
591                             transactionOrder = DetectedTransactionOrder.FILE;
592                             debug("rtt", String.format(Locale.ENGLISH,
593                                     "Detected native file order after %d transactions (factor %d)",
594                                     processedTransactionCount, orderAccumulator));
595                             progress.setTotal(Data.transactions.size());
596                         }
597                         else if (orderAccumulator < -30) {
598                             transactionOrder = DetectedTransactionOrder.REVERSE_CHRONOLOGICAL;
599                             debug("rtt", String.format(Locale.ENGLISH,
600                                     "Detected reverse chronological order after %d transactions " +
601                                     "(factor %d)", processedTransactionCount, orderAccumulator));
602                         }
603                     }
604
605                     if (transaction.existsInDb(db)) {
606                         profile.markTransactionAsPresent(db, transaction);
607                         matchedTransactionsCount++;
608
609                         if ((transactionOrder == DetectedTransactionOrder.REVERSE_CHRONOLOGICAL) &&
610                             (matchedTransactionsCount == MATCHING_TRANSACTIONS_LIMIT))
611                         {
612                             profile.markTransactionsBeforeTransactionAsPresent(db, transaction);
613                             progress.setTotal(progress.getProgress());
614                             publishProgress(progress);
615                             db.setTransactionSuccessful();
616                             profile.setLastUpdateStamp();
617                             return true;
618                         }
619                     }
620                     else {
621                         profile.storeTransaction(db, transaction);
622                         matchedTransactionsCount = 0;
623                         progress.setTotal(maxTransactionId);
624                     }
625
626
627                     if ((transactionOrder != DetectedTransactionOrder.UNKNOWN) &&
628                         ((progress.getTotal() == Progress.INDETERMINATE) ||
629                          (progress.getTotal() < transaction.getId())))
630                         progress.setTotal(transaction.getId());
631
632                     progress.setProgress(++processedTransactionCount);
633                     publishProgress(progress);
634                 }
635
636                 throwIfCancelled();
637                 profile.deleteNotPresentTransactions(db);
638                 throwIfCancelled();
639                 db.setTransactionSuccessful();
640                 profile.setLastUpdateStamp();
641             }
642             finally {
643                 db.endTransaction();
644             }
645         }
646
647         return true;
648     }
649
650     @SuppressLint("DefaultLocale")
651     @Override
652     protected String doInBackground(Void... params) {
653         Data.backgroundTaskStarted();
654         try {
655             if (!retrieveAccountList() || !retrieveTransactionList())
656                 return retrieveTransactionListLegacy();
657             return null;
658         }
659         catch (MalformedURLException e) {
660             e.printStackTrace();
661             return "Invalid server URL";
662         }
663         catch (HTTPException e) {
664             e.printStackTrace();
665             return String.format("HTTP error %d: %s", e.getResponseCode(), e.getResponseMessage());
666         }
667         catch (IOException e) {
668             e.printStackTrace();
669             return e.getLocalizedMessage();
670         }
671         catch (ParseException e) {
672             e.printStackTrace();
673             return "Network error";
674         }
675         catch (OperationCanceledException e) {
676             e.printStackTrace();
677             return "Operation cancelled";
678         }
679         finally {
680             Data.backgroundTaskFinished();
681         }
682     }
683     private MainActivity getContext() {
684         return contextRef.get();
685     }
686     private void throwIfCancelled() {
687         if (isCancelled())
688             throw new OperationCanceledException(null);
689     }
690     enum DetectedTransactionOrder {UNKNOWN, REVERSE_CHRONOLOGICAL, FILE}
691
692     private enum ParserState {
693         EXPECTING_ACCOUNT, EXPECTING_ACCOUNT_AMOUNT, EXPECTING_JOURNAL, EXPECTING_TRANSACTION,
694         EXPECTING_TRANSACTION_DESCRIPTION, EXPECTING_TRANSACTION_DETAILS
695     }
696
697     public class Progress {
698         public static final int INDETERMINATE = -1;
699         private int progress;
700         private int total;
701         Progress() {
702             this(INDETERMINATE, INDETERMINATE);
703         }
704         Progress(int progress, int total) {
705             this.progress = progress;
706             this.total = total;
707         }
708         public int getProgress() {
709             return progress;
710         }
711         protected void setProgress(int progress) {
712             this.progress = progress;
713         }
714         public int getTotal() {
715             return total;
716         }
717         protected void setTotal(int total) {
718             this.total = total;
719         }
720     }
721
722     private class TransactionParserException extends IllegalStateException {
723         TransactionParserException(String message) {
724             super(message);
725         }
726     }
727 }