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