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