]> git.ktnx.net Git - mobile-ledger.git/blob - app/src/main/java/net/ktnx/mobileledger/model/MobileLedgerProfile.java
55746dddd1279f803798357fd4441190ea5f9452
[mobile-ledger.git] / app / src / main / java / net / ktnx / mobileledger / model / MobileLedgerProfile.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.model;
19
20 import android.content.res.Resources;
21 import android.database.Cursor;
22 import android.database.sqlite.SQLiteDatabase;
23 import android.text.TextUtils;
24 import android.util.SparseArray;
25
26 import androidx.annotation.Nullable;
27
28 import net.ktnx.mobileledger.App;
29 import net.ktnx.mobileledger.R;
30 import net.ktnx.mobileledger.async.DbOpQueue;
31 import net.ktnx.mobileledger.async.SendTransactionTask;
32 import net.ktnx.mobileledger.utils.Logger;
33 import net.ktnx.mobileledger.utils.Misc;
34 import net.ktnx.mobileledger.utils.SimpleDate;
35
36 import org.jetbrains.annotations.Contract;
37
38 import java.util.ArrayList;
39 import java.util.HashMap;
40 import java.util.List;
41 import java.util.Locale;
42 import java.util.Map;
43 import java.util.Objects;
44
45 import static net.ktnx.mobileledger.utils.Logger.debug;
46
47 public final class MobileLedgerProfile {
48     // N.B. when adding new fields, update the copy-constructor below
49     private final String uuid;
50     private String name;
51     private boolean permitPosting;
52     private boolean showCommentsByDefault;
53     private boolean showCommodityByDefault;
54     private String defaultCommodity;
55     private String preferredAccountsFilter;
56     private String url;
57     private boolean authEnabled;
58     private String authUserName;
59     private String authPassword;
60     private int themeHue;
61     private int orderNo = -1;
62     private SendTransactionTask.API apiVersion = SendTransactionTask.API.auto;
63     private FutureDates futureDates = FutureDates.None;
64     private boolean accountsLoaded;
65     private boolean transactionsLoaded;
66     private HledgerVersion detectedVersion;
67     // N.B. when adding new fields, update the copy-constructor below
68     transient private AccountAndTransactionListSaver accountAndTransactionListSaver;
69     public MobileLedgerProfile(String uuid) {
70         this.uuid = uuid;
71     }
72     public MobileLedgerProfile(MobileLedgerProfile origin) {
73         uuid = origin.uuid;
74         name = origin.name;
75         permitPosting = origin.permitPosting;
76         showCommentsByDefault = origin.showCommentsByDefault;
77         showCommodityByDefault = origin.showCommodityByDefault;
78         preferredAccountsFilter = origin.preferredAccountsFilter;
79         url = origin.url;
80         authEnabled = origin.authEnabled;
81         authUserName = origin.authUserName;
82         authPassword = origin.authPassword;
83         themeHue = origin.themeHue;
84         orderNo = origin.orderNo;
85         futureDates = origin.futureDates;
86         apiVersion = origin.apiVersion;
87         defaultCommodity = origin.defaultCommodity;
88         accountsLoaded = origin.accountsLoaded;
89         transactionsLoaded = origin.transactionsLoaded;
90         if (origin.detectedVersion != null)
91             detectedVersion = new HledgerVersion(origin.detectedVersion);
92     }
93     // loads all profiles into Data.profiles
94     // returns the profile with the given UUID
95     public static MobileLedgerProfile loadAllFromDB(@Nullable String currentProfileUUID) {
96         MobileLedgerProfile result = null;
97         ArrayList<MobileLedgerProfile> list = new ArrayList<>();
98         SQLiteDatabase db = App.getDatabase();
99         try (Cursor cursor = db.rawQuery("SELECT uuid, name, url, use_authentication, auth_user, " +
100                                          "auth_password, permit_posting, theme, order_no, " +
101                                          "preferred_accounts_filter, future_dates, api_version, " +
102                                          "show_commodity_by_default, default_commodity, " +
103                                          "show_comments_by_default, detected_version_pre_1_19, " +
104                                          "detected_version_major, detected_version_minor FROM " +
105                                          "profiles order by order_no", null))
106         {
107             while (cursor.moveToNext()) {
108                 MobileLedgerProfile item = new MobileLedgerProfile(cursor.getString(0));
109                 item.setName(cursor.getString(1));
110                 item.setUrl(cursor.getString(2));
111                 item.setAuthEnabled(cursor.getInt(3) == 1);
112                 item.setAuthUserName(cursor.getString(4));
113                 item.setAuthPassword(cursor.getString(5));
114                 item.setPostingPermitted(cursor.getInt(6) == 1);
115                 item.setThemeId(cursor.getInt(7));
116                 item.orderNo = cursor.getInt(8);
117                 item.setPreferredAccountsFilter(cursor.getString(9));
118                 item.setFutureDates(cursor.getInt(10));
119                 item.setApiVersion(cursor.getInt(11));
120                 item.setShowCommodityByDefault(cursor.getInt(12) == 1);
121                 item.setDefaultCommodity(cursor.getString(13));
122                 item.setShowCommentsByDefault(cursor.getInt(14) == 1);
123                 {
124                     boolean pre_1_20 = cursor.getInt(15) == 1;
125                     int major = cursor.getInt(16);
126                     int minor = cursor.getInt(17);
127
128                     if (!pre_1_20 && major == 0 && minor == 0) {
129                         item.detectedVersion = null;
130                     }
131                     else if (pre_1_20) {
132                         item.detectedVersion = new HledgerVersion(true);
133                     }
134                     else {
135                         item.detectedVersion = new HledgerVersion(major, minor);
136                     }
137                 }
138                 list.add(item);
139                 if (item.getUuid()
140                         .equals(currentProfileUUID))
141                     result = item;
142             }
143         }
144         Data.profiles.postValue(list);
145         return result;
146     }
147     public static void storeProfilesOrder() {
148         SQLiteDatabase db = App.getDatabase();
149         db.beginTransactionNonExclusive();
150         try {
151             int orderNo = 0;
152             for (MobileLedgerProfile p : Objects.requireNonNull(Data.profiles.getValue())) {
153                 db.execSQL("update profiles set order_no=? where uuid=?",
154                         new Object[]{orderNo, p.getUuid()});
155                 p.orderNo = orderNo;
156                 orderNo++;
157             }
158             db.setTransactionSuccessful();
159         }
160         finally {
161             db.endTransaction();
162         }
163     }
164     public HledgerVersion getDetectedVersion() {
165         return detectedVersion;
166     }
167     public void setDetectedVersion(HledgerVersion detectedVersion) {
168         this.detectedVersion = detectedVersion;
169     }
170     @Contract(value = "null -> false", pure = true)
171     @Override
172     public boolean equals(@Nullable Object obj) {
173         if (obj == null)
174             return false;
175         if (obj == this)
176             return true;
177         if (obj.getClass() != this.getClass())
178             return false;
179
180         MobileLedgerProfile p = (MobileLedgerProfile) obj;
181         if (!uuid.equals(p.uuid))
182             return false;
183         if (!name.equals(p.name))
184             return false;
185         if (permitPosting != p.permitPosting)
186             return false;
187         if (showCommentsByDefault != p.showCommentsByDefault)
188             return false;
189         if (showCommodityByDefault != p.showCommodityByDefault)
190             return false;
191         if (!Objects.equals(defaultCommodity, p.defaultCommodity))
192             return false;
193         if (!Objects.equals(preferredAccountsFilter, p.preferredAccountsFilter))
194             return false;
195         if (!Objects.equals(url, p.url))
196             return false;
197         if (authEnabled != p.authEnabled)
198             return false;
199         if (!Objects.equals(authUserName, p.authUserName))
200             return false;
201         if (!Objects.equals(authPassword, p.authPassword))
202             return false;
203         if (themeHue != p.themeHue)
204             return false;
205         if (apiVersion != p.apiVersion)
206             return false;
207         if (!Objects.equals(detectedVersion, p.detectedVersion))
208             return false;
209         return futureDates == p.futureDates;
210     }
211     public boolean getShowCommentsByDefault() {
212         return showCommentsByDefault;
213     }
214     public void setShowCommentsByDefault(boolean newValue) {
215         this.showCommentsByDefault = newValue;
216     }
217     public boolean getShowCommodityByDefault() {
218         return showCommodityByDefault;
219     }
220     public void setShowCommodityByDefault(boolean showCommodityByDefault) {
221         this.showCommodityByDefault = showCommodityByDefault;
222     }
223     public String getDefaultCommodity() {
224         return defaultCommodity;
225     }
226     public void setDefaultCommodity(String defaultCommodity) {
227         this.defaultCommodity = defaultCommodity;
228     }
229     public void setDefaultCommodity(CharSequence defaultCommodity) {
230         if (defaultCommodity == null)
231             this.defaultCommodity = null;
232         else
233             this.defaultCommodity = String.valueOf(defaultCommodity);
234     }
235     public SendTransactionTask.API getApiVersion() {
236         return apiVersion;
237     }
238     public void setApiVersion(SendTransactionTask.API apiVersion) {
239         this.apiVersion = apiVersion;
240     }
241     public void setApiVersion(int apiVersion) {
242         this.apiVersion = SendTransactionTask.API.valueOf(apiVersion);
243     }
244     public FutureDates getFutureDates() {
245         return futureDates;
246     }
247     public void setFutureDates(int anInt) {
248         futureDates = FutureDates.valueOf(anInt);
249     }
250     public void setFutureDates(FutureDates futureDates) {
251         this.futureDates = futureDates;
252     }
253     public String getPreferredAccountsFilter() {
254         return preferredAccountsFilter;
255     }
256     public void setPreferredAccountsFilter(String preferredAccountsFilter) {
257         this.preferredAccountsFilter = preferredAccountsFilter;
258     }
259     public void setPreferredAccountsFilter(CharSequence preferredAccountsFilter) {
260         setPreferredAccountsFilter(String.valueOf(preferredAccountsFilter));
261     }
262     public boolean isPostingPermitted() {
263         return permitPosting;
264     }
265     public void setPostingPermitted(boolean permitPosting) {
266         this.permitPosting = permitPosting;
267     }
268     public String getUuid() {
269         return uuid;
270     }
271     public String getName() {
272         return name;
273     }
274     public void setName(CharSequence text) {
275         setName(String.valueOf(text));
276     }
277     public void setName(String name) {
278         this.name = name;
279     }
280     public String getUrl() {
281         return url;
282     }
283     public void setUrl(CharSequence text) {
284         setUrl(String.valueOf(text));
285     }
286     public void setUrl(String url) {
287         this.url = url;
288     }
289     public boolean isAuthEnabled() {
290         return authEnabled;
291     }
292     public void setAuthEnabled(boolean authEnabled) {
293         this.authEnabled = authEnabled;
294     }
295     public String getAuthUserName() {
296         return authUserName;
297     }
298     public void setAuthUserName(CharSequence text) {
299         setAuthUserName(String.valueOf(text));
300     }
301     public void setAuthUserName(String authUserName) {
302         this.authUserName = authUserName;
303     }
304     public String getAuthPassword() {
305         return authPassword;
306     }
307     public void setAuthPassword(CharSequence text) {
308         setAuthPassword(String.valueOf(text));
309     }
310     public void setAuthPassword(String authPassword) {
311         this.authPassword = authPassword;
312     }
313     public void storeInDB() {
314         SQLiteDatabase db = App.getDatabase();
315         db.beginTransactionNonExclusive();
316         try {
317 //            debug("profiles", String.format("Storing profile in DB: uuid=%s, name=%s, " +
318 //                                            "url=%s, permit_posting=%s, authEnabled=%s, " +
319 //                                            "themeHue=%d", uuid, name, url,
320 //                    permitPosting ? "TRUE" : "FALSE", authEnabled ? "TRUE" : "FALSE", themeHue));
321             db.execSQL("REPLACE INTO profiles(uuid, name, permit_posting, url, " +
322                        "use_authentication, auth_user, auth_password, theme, order_no, " +
323                        "preferred_accounts_filter, future_dates, api_version, " +
324                        "show_commodity_by_default, default_commodity, show_comments_by_default," +
325                        "detected_version_pre_1_19, detected_version_major, " +
326                        "detected_version_minor) " +
327                        "VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
328                     new Object[]{uuid, name, permitPosting, url, authEnabled,
329                                  authEnabled ? authUserName : null,
330                                  authEnabled ? authPassword : null, themeHue, orderNo,
331                                  preferredAccountsFilter, futureDates.toInt(), apiVersion.toInt(),
332                                  showCommodityByDefault, defaultCommodity, showCommentsByDefault,
333                                  (detectedVersion != null) && detectedVersion.isPre_1_20(),
334                                  (detectedVersion == null) ? 0 : detectedVersion.getMajor(),
335                                  (detectedVersion == null) ? 0 : detectedVersion.getMinor()
336                     });
337             db.setTransactionSuccessful();
338         }
339         finally {
340             db.endTransaction();
341         }
342     }
343     public void storeAccount(SQLiteDatabase db, int generation, LedgerAccount acc,
344                              boolean storeUiFields) {
345         // replace into is a bad idea because it would reset hidden to its default value
346         // we like the default, but for new accounts only
347         String sql = "update accounts set generation = ?";
348         List<Object> params = new ArrayList<>();
349         params.add(generation);
350         if (storeUiFields) {
351             sql += ", expanded=?";
352             params.add(acc.isExpanded() ? 1 : 0);
353         }
354         sql += " where profile=? and name=?";
355         params.add(uuid);
356         params.add(acc.getName());
357         db.execSQL(sql, params.toArray());
358
359         db.execSQL("insert into accounts(profile, name, name_upper, parent_name, level, " +
360                    "expanded, generation) select ?,?,?,?,?,0,? where (select changes() = 0)",
361                 new Object[]{uuid, acc.getName(), acc.getName().toUpperCase(), acc.getParentName(),
362                              acc.getLevel(), generation
363                 });
364 //        debug("accounts", String.format("Stored account '%s' in DB [%s]", acc.getName(), uuid));
365     }
366     public void storeAccountValue(SQLiteDatabase db, int generation, String name, String currency,
367                                   Float amount) {
368         if (!TextUtils.isEmpty(currency)) {
369             boolean exists;
370             try (Cursor c = db.rawQuery("select 1 from currencies where name=?",
371                     new String[]{currency}))
372             {
373                 exists = c.moveToFirst();
374             }
375             if (!exists) {
376                 db.execSQL(
377                         "insert into currencies(id, name, position, has_gap) values((select max" +
378                         "(id) from currencies)+1, ?, ?, ?)", new Object[]{currency,
379                                                                           Objects.requireNonNull(
380                                                                                   Data.currencySymbolPosition.getValue()).toString(),
381                                                                           Data.currencyGap.getValue()
382                         });
383             }
384         }
385
386         db.execSQL("replace into account_values(profile, account, " +
387                    "currency, value, generation) values(?, ?, ?, ?, ?);",
388                 new Object[]{uuid, name, Misc.emptyIsNull(currency), amount, generation});
389     }
390     public void storeTransaction(SQLiteDatabase db, int generation, LedgerTransaction tr) {
391         tr.fillDataHash();
392 //        Logger.debug("storeTransaction", String.format(Locale.US, "ID %d", tr.getId()));
393         SimpleDate d = tr.getDate();
394         db.execSQL("UPDATE transactions SET year=?, month=?, day=?, description=?, comment=?, " +
395                    "data_hash=?, generation=? WHERE profile=? AND id=?",
396                 new Object[]{d.year, d.month, d.day, tr.getDescription(), tr.getComment(),
397                              tr.getDataHash(), generation, uuid, tr.getId()
398                 });
399         db.execSQL("INSERT INTO transactions(profile, id, year, month, day, description, " +
400                    "comment, data_hash, generation) " +
401                    "select ?,?,?,?,?,?,?,?,? WHERE (select changes() = 0)",
402                 new Object[]{uuid, tr.getId(), tr.getDate().year, tr.getDate().month,
403                              tr.getDate().day, tr.getDescription(), tr.getComment(),
404                              tr.getDataHash(), generation
405                 });
406
407         int accountOrderNo = 1;
408         for (LedgerTransactionAccount item : tr.getAccounts()) {
409             db.execSQL("UPDATE transaction_accounts SET account_name=?, amount=?, currency=?, " +
410                        "comment=?, generation=? " +
411                        "WHERE profile=? AND transaction_id=? AND order_no=?",
412                     new Object[]{item.getAccountName(), item.getAmount(),
413                                  Misc.nullIsEmpty(item.getCurrency()), item.getComment(),
414                                  generation, uuid, tr.getId(), accountOrderNo
415                     });
416             db.execSQL("INSERT INTO transaction_accounts(profile, transaction_id, " +
417                        "order_no, account_name, amount, currency, comment, generation) " +
418                        "select ?, ?, ?, ?, ?, ?, ?, ? WHERE (select changes() = 0)",
419                     new Object[]{uuid, tr.getId(), accountOrderNo, item.getAccountName(),
420                                  item.getAmount(), Misc.nullIsEmpty(item.getCurrency()),
421                                  item.getComment(), generation
422                     });
423
424             accountOrderNo++;
425         }
426 //        debug("profile", String.format("Transaction %d stored", tr.getId()));
427     }
428     public String getOption(String name, String default_value) {
429         SQLiteDatabase db = App.getDatabase();
430         try (Cursor cursor = db.rawQuery("select value from options where profile = ? and name=?",
431                 new String[]{uuid, name}))
432         {
433             if (cursor.moveToFirst()) {
434                 String result = cursor.getString(0);
435
436                 if (result == null) {
437                     debug("profile", "returning default value for " + name);
438                     result = default_value;
439                 }
440                 else
441                     debug("profile", String.format("option %s=%s", name, result));
442
443                 return result;
444             }
445             else
446                 return default_value;
447         }
448         catch (Exception e) {
449             debug("db", "returning default value for " + name, e);
450             return default_value;
451         }
452     }
453     public long getLongOption(String name, long default_value) {
454         long longResult;
455         String result = getOption(name, "");
456         if ((result == null) || result.isEmpty()) {
457             debug("profile", String.format("Returning default value for option %s", name));
458             longResult = default_value;
459         }
460         else {
461             try {
462                 longResult = Long.parseLong(result);
463                 debug("profile", String.format("option %s=%s", name, result));
464             }
465             catch (Exception e) {
466                 debug("profile", String.format("Returning default value for option %s", name), e);
467                 longResult = default_value;
468             }
469         }
470
471         return longResult;
472     }
473     public void setOption(String name, String value) {
474         debug("profile", String.format("setting option %s=%s", name, value));
475         DbOpQueue.add("insert or replace into options(profile, name, value) values(?, ?, ?);",
476                 new String[]{uuid, name, value});
477     }
478     public void setLongOption(String name, long value) {
479         setOption(name, String.valueOf(value));
480     }
481     public void removeFromDB() {
482         SQLiteDatabase db = App.getDatabase();
483         debug("db", String.format("removing profile %s from DB", uuid));
484         db.beginTransactionNonExclusive();
485         try {
486             Object[] uuid_param = new Object[]{uuid};
487             db.execSQL("delete from transaction_accounts where profile=?", uuid_param);
488             db.execSQL("delete from transactions where profile=?", uuid_param);
489             db.execSQL("delete from account_values where profile=?", uuid_param);
490             db.execSQL("delete from accounts where profile=?", uuid_param);
491             db.execSQL("delete from options where profile=?", uuid_param);
492             db.execSQL("delete from profiles where uuid=?", uuid_param);
493             db.setTransactionSuccessful();
494         }
495         finally {
496             db.endTransaction();
497         }
498     }
499     public LedgerTransaction loadTransaction(int transactionId) {
500         LedgerTransaction tr = new LedgerTransaction(transactionId, this.uuid);
501         tr.loadData(App.getDatabase());
502
503         return tr;
504     }
505     public int getThemeHue() {
506 //        debug("profile", String.format("Profile.getThemeHue() returning %d", themeHue));
507         return this.themeHue;
508     }
509     public void setThemeHue(Object o) {
510         setThemeId(Integer.parseInt(String.valueOf(o)));
511     }
512     public void setThemeId(int themeHue) {
513 //        debug("profile", String.format("Profile.setThemeHue(%d) called", themeHue));
514         this.themeHue = themeHue;
515     }
516     public int getNextTransactionsGeneration(SQLiteDatabase db) {
517         int generation = 1;
518         try (Cursor c = db.rawQuery("SELECT generation FROM transactions WHERE profile=? LIMIT 1",
519                 new String[]{uuid}))
520         {
521             if (c.moveToFirst()) {
522                 generation = c.getInt(0) + 1;
523             }
524         }
525         return generation;
526     }
527     private int getNextAccountsGeneration(SQLiteDatabase db) {
528         int generation = 1;
529         try (Cursor c = db.rawQuery("SELECT generation FROM accounts WHERE profile=? LIMIT 1",
530                 new String[]{uuid}))
531         {
532             if (c.moveToFirst()) {
533                 generation = c.getInt(0) + 1;
534             }
535         }
536         return generation;
537     }
538     private void deleteNotPresentAccounts(SQLiteDatabase db, int generation) {
539         Logger.debug("db/benchmark", "Deleting obsolete accounts");
540         db.execSQL("DELETE FROM account_values WHERE profile=? AND generation <> ?",
541                 new Object[]{uuid, generation});
542         db.execSQL("DELETE FROM accounts WHERE profile=? AND generation <> ?",
543                 new Object[]{uuid, generation});
544         Logger.debug("db/benchmark", "Done deleting obsolete accounts");
545     }
546     private void deleteNotPresentTransactions(SQLiteDatabase db, int generation) {
547         Logger.debug("db/benchmark", "Deleting obsolete transactions");
548         db.execSQL("DELETE FROM transaction_accounts WHERE profile=? AND generation <> ?",
549                 new Object[]{uuid, generation});
550         db.execSQL("DELETE FROM transactions WHERE profile=? AND generation <> ?",
551                 new Object[]{uuid, generation});
552         Logger.debug("db/benchmark", "Done deleting obsolete transactions");
553     }
554     public void wipeAllData() {
555         SQLiteDatabase db = App.getDatabase();
556         db.beginTransaction();
557         try {
558             String[] pUuid = new String[]{uuid};
559             db.execSQL("delete from options where profile=?", pUuid);
560             db.execSQL("delete from accounts where profile=?", pUuid);
561             db.execSQL("delete from account_values where profile=?", pUuid);
562             db.execSQL("delete from transactions where profile=?", pUuid);
563             db.execSQL("delete from transaction_accounts where profile=?", pUuid);
564             db.setTransactionSuccessful();
565             debug("wipe", String.format(Locale.ENGLISH, "Profile %s wiped out", pUuid[0]));
566         }
567         finally {
568             db.endTransaction();
569         }
570     }
571     public List<Currency> getCurrencies() {
572         SQLiteDatabase db = App.getDatabase();
573
574         ArrayList<Currency> result = new ArrayList<>();
575
576         try (Cursor c = db.rawQuery("SELECT c.id, c.name, c.position, c.has_gap FROM currencies c",
577                 new String[]{}))
578         {
579             while (c.moveToNext()) {
580                 Currency currency = new Currency(c.getInt(0), c.getString(1),
581                         Currency.Position.valueOf(c.getString(2)), c.getInt(3) == 1);
582                 result.add(currency);
583             }
584         }
585
586         return result;
587     }
588     Currency loadCurrencyByName(String name) {
589         SQLiteDatabase db = App.getDatabase();
590         Currency result = tryLoadCurrencyByName(db, name);
591         if (result == null)
592             throw new RuntimeException(String.format("Unable to load currency '%s'", name));
593         return result;
594     }
595     private Currency tryLoadCurrencyByName(SQLiteDatabase db, String name) {
596         try (Cursor cursor = db.rawQuery(
597                 "SELECT c.id, c.name, c.position, c.has_gap FROM currencies c WHERE c.name=?",
598                 new String[]{name}))
599         {
600             if (cursor.moveToFirst()) {
601                 return new Currency(cursor.getInt(0), cursor.getString(1),
602                         Currency.Position.valueOf(cursor.getString(2)), cursor.getInt(3) == 1);
603             }
604             return null;
605         }
606     }
607     public void storeAccountAndTransactionListAsync(List<LedgerAccount> accounts,
608                                                     List<LedgerTransaction> transactions) {
609         if (accountAndTransactionListSaver != null)
610             accountAndTransactionListSaver.interrupt();
611
612         accountAndTransactionListSaver =
613                 new AccountAndTransactionListSaver(this, accounts, transactions);
614         accountAndTransactionListSaver.start();
615     }
616
617     public enum FutureDates {
618         None(0), OneWeek(7), TwoWeeks(14), OneMonth(30), TwoMonths(60), ThreeMonths(90),
619         SixMonths(180), OneYear(365), All(-1);
620         private static final SparseArray<FutureDates> map = new SparseArray<>();
621
622         static {
623             for (FutureDates item : FutureDates.values()) {
624                 map.put(item.value, item);
625             }
626         }
627
628         private int value;
629         FutureDates(int value) {
630             this.value = value;
631         }
632         public static FutureDates valueOf(int i) {
633             return map.get(i, None);
634         }
635         public int toInt() {
636             return this.value;
637         }
638         public String getText(Resources resources) {
639             switch (value) {
640                 case 7:
641                     return resources.getString(R.string.future_dates_7);
642                 case 14:
643                     return resources.getString(R.string.future_dates_14);
644                 case 30:
645                     return resources.getString(R.string.future_dates_30);
646                 case 60:
647                     return resources.getString(R.string.future_dates_60);
648                 case 90:
649                     return resources.getString(R.string.future_dates_90);
650                 case 180:
651                     return resources.getString(R.string.future_dates_180);
652                 case 365:
653                     return resources.getString(R.string.future_dates_365);
654                 case -1:
655                     return resources.getString(R.string.future_dates_all);
656                 default:
657                     return resources.getString(R.string.future_dates_none);
658             }
659         }
660     }
661
662     private static class AccountAndTransactionListSaver extends Thread {
663         private final MobileLedgerProfile profile;
664         private final List<LedgerAccount> accounts;
665         private final List<LedgerTransaction> transactions;
666         AccountAndTransactionListSaver(MobileLedgerProfile profile, List<LedgerAccount> accounts,
667                                        List<LedgerTransaction> transactions) {
668             this.accounts = accounts;
669             this.transactions = transactions;
670             this.profile = profile;
671         }
672         public int getNextDescriptionsGeneration(SQLiteDatabase db) {
673             int generation = 1;
674             try (Cursor c = db.rawQuery("SELECT generation FROM description_history LIMIT 1",
675                     null))
676             {
677                 if (c.moveToFirst()) {
678                     generation = c.getInt(0) + 1;
679                 }
680             }
681             return generation;
682         }
683         void deleteNotPresentDescriptions(SQLiteDatabase db, int generation) {
684             Logger.debug("db/benchmark", "Deleting obsolete descriptions");
685             db.execSQL("DELETE FROM description_history WHERE generation <> ?",
686                     new Object[]{generation});
687             db.execSQL("DELETE FROM description_history WHERE generation <> ?",
688                     new Object[]{generation});
689             Logger.debug("db/benchmark", "Done deleting obsolete descriptions");
690         }
691         @Override
692         public void run() {
693             SQLiteDatabase db = App.getDatabase();
694             db.beginTransactionNonExclusive();
695             try {
696                 int accountsGeneration = profile.getNextAccountsGeneration(db);
697                 if (isInterrupted())
698                     return;
699
700                 int transactionsGeneration = profile.getNextTransactionsGeneration(db);
701                 if (isInterrupted())
702                     return;
703
704                 for (LedgerAccount acc : accounts) {
705                     profile.storeAccount(db, accountsGeneration, acc, false);
706                     if (isInterrupted())
707                         return;
708                     for (LedgerAmount amt : acc.getAmounts()) {
709                         profile.storeAccountValue(db, accountsGeneration, acc.getName(),
710                                 amt.getCurrency(), amt.getAmount());
711                         if (isInterrupted())
712                             return;
713                     }
714                 }
715
716                 for (LedgerTransaction tr : transactions) {
717                     profile.storeTransaction(db, transactionsGeneration, tr);
718                     if (isInterrupted())
719                         return;
720                 }
721
722                 profile.deleteNotPresentTransactions(db, transactionsGeneration);
723                 if (isInterrupted()) {
724                     return;
725                 }
726                 profile.deleteNotPresentAccounts(db, accountsGeneration);
727                 if (isInterrupted())
728                     return;
729
730                 Map<String, Boolean> unique = new HashMap<>();
731
732                 debug("descriptions", "Starting refresh");
733                 int descriptionsGeneration = getNextDescriptionsGeneration(db);
734                 try (Cursor c = db.rawQuery("SELECT distinct description from transactions",
735                         null))
736                 {
737                     while (c.moveToNext()) {
738                         String description = c.getString(0);
739                         String descriptionUpper = description.toUpperCase();
740                         if (unique.containsKey(descriptionUpper))
741                             continue;
742
743                         storeDescription(db, descriptionsGeneration, description, descriptionUpper);
744
745                         unique.put(descriptionUpper, true);
746                     }
747                 }
748                 deleteNotPresentDescriptions(db, descriptionsGeneration);
749
750                 db.setTransactionSuccessful();
751             }
752             finally {
753                 db.endTransaction();
754             }
755         }
756         private void storeDescription(SQLiteDatabase db, int generation, String description,
757                                       String descriptionUpper) {
758             db.execSQL("UPDATE description_history SET description=?, generation=? WHERE " +
759                        "description_upper=?", new Object[]{description, generation, descriptionUpper
760             });
761             db.execSQL(
762                     "INSERT INTO description_history(description, description_upper, generation) " +
763                     "select ?,?,? WHERE (select changes() = 0)",
764                     new Object[]{description, descriptionUpper, generation
765                     });
766         }
767     }
768 }