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.
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.
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/>.
18 package net.ktnx.mobileledger.model;
20 import android.content.res.Resources;
21 import android.database.Cursor;
22 import android.database.sqlite.SQLiteDatabase;
23 import android.os.Build;
24 import android.text.TextUtils;
25 import android.util.SparseArray;
27 import androidx.annotation.Nullable;
28 import androidx.lifecycle.LiveData;
29 import androidx.lifecycle.MutableLiveData;
31 import net.ktnx.mobileledger.App;
32 import net.ktnx.mobileledger.R;
33 import net.ktnx.mobileledger.async.DbOpQueue;
34 import net.ktnx.mobileledger.async.SendTransactionTask;
35 import net.ktnx.mobileledger.utils.LockHolder;
36 import net.ktnx.mobileledger.utils.Locker;
37 import net.ktnx.mobileledger.utils.Logger;
38 import net.ktnx.mobileledger.utils.MLDB;
39 import net.ktnx.mobileledger.utils.Misc;
41 import org.jetbrains.annotations.Contract;
43 import java.util.ArrayList;
44 import java.util.Calendar;
45 import java.util.Date;
46 import java.util.HashMap;
47 import java.util.Iterator;
48 import java.util.List;
49 import java.util.Locale;
51 import java.util.Objects;
53 import static net.ktnx.mobileledger.utils.Logger.debug;
55 public final class MobileLedgerProfile {
56 private final MutableLiveData<List<LedgerAccount>> displayedAccounts;
57 private final MutableLiveData<List<LedgerTransaction>> allTransactions;
58 private final MutableLiveData<List<LedgerTransaction>> displayedTransactions;
59 // N.B. when adding new fields, update the copy-constructor below
60 private final String uuid;
61 private final Locker accountsLocker = new Locker();
62 private List<LedgerAccount> allAccounts;
64 private boolean permitPosting;
65 private boolean showCommentsByDefault;
66 private boolean showCommodityByDefault;
67 private String defaultCommodity;
68 private String preferredAccountsFilter;
70 private boolean authEnabled;
71 private String authUserName;
72 private String authPassword;
74 private int orderNo = -1;
75 private SendTransactionTask.API apiVersion = SendTransactionTask.API.auto;
76 private Calendar firstTransactionDate;
77 private Calendar lastTransactionDate;
78 private FutureDates futureDates = FutureDates.None;
79 private boolean accountsLoaded;
80 private boolean transactionsLoaded;
81 // N.B. when adding new fields, update the copy-constructor below
82 transient private AccountListLoader loader = null;
83 transient private Thread displayedAccountsUpdater;
84 transient private AccountListSaver accountListSaver;
85 transient private TransactionListSaver transactionListSaver;
86 transient private AccountAndTransactionListSaver accountAndTransactionListSaver;
87 private Map<String, LedgerAccount> accountMap = new HashMap<>();
88 public MobileLedgerProfile(String uuid) {
90 allAccounts = new ArrayList<>();
91 displayedAccounts = new MutableLiveData<>();
92 allTransactions = new MutableLiveData<>(new ArrayList<>());
93 displayedTransactions = new MutableLiveData<>(new ArrayList<>());
95 public MobileLedgerProfile(MobileLedgerProfile origin) {
98 permitPosting = origin.permitPosting;
99 showCommentsByDefault = origin.showCommentsByDefault;
100 showCommodityByDefault = origin.showCommodityByDefault;
101 preferredAccountsFilter = origin.preferredAccountsFilter;
103 authEnabled = origin.authEnabled;
104 authUserName = origin.authUserName;
105 authPassword = origin.authPassword;
106 themeHue = origin.themeHue;
107 orderNo = origin.orderNo;
108 futureDates = origin.futureDates;
109 apiVersion = origin.apiVersion;
110 defaultCommodity = origin.defaultCommodity;
111 firstTransactionDate = origin.firstTransactionDate;
112 lastTransactionDate = origin.lastTransactionDate;
113 displayedAccounts = origin.displayedAccounts;
114 allAccounts = origin.allAccounts;
115 accountMap = origin.accountMap;
116 displayedTransactions = origin.displayedTransactions;
117 allTransactions = origin.allTransactions;
118 accountsLoaded = origin.accountsLoaded;
119 transactionsLoaded = origin.transactionsLoaded;
121 // loads all profiles into Data.profiles
122 // returns the profile with the given UUID
123 public static MobileLedgerProfile loadAllFromDB(@Nullable String currentProfileUUID) {
124 MobileLedgerProfile result = null;
125 ArrayList<MobileLedgerProfile> list = new ArrayList<>();
126 SQLiteDatabase db = App.getDatabase();
127 try (Cursor cursor = db.rawQuery("SELECT uuid, name, url, use_authentication, auth_user, " +
128 "auth_password, permit_posting, theme, order_no, " +
129 "preferred_accounts_filter, future_dates, api_version, " +
130 "show_commodity_by_default, default_commodity, " +
131 "show_comments_by_default FROM " +
132 "profiles order by order_no", null))
134 while (cursor.moveToNext()) {
135 MobileLedgerProfile item = new MobileLedgerProfile(cursor.getString(0));
136 item.setName(cursor.getString(1));
137 item.setUrl(cursor.getString(2));
138 item.setAuthEnabled(cursor.getInt(3) == 1);
139 item.setAuthUserName(cursor.getString(4));
140 item.setAuthPassword(cursor.getString(5));
141 item.setPostingPermitted(cursor.getInt(6) == 1);
142 item.setThemeId(cursor.getInt(7));
143 item.orderNo = cursor.getInt(8);
144 item.setPreferredAccountsFilter(cursor.getString(9));
145 item.setFutureDates(cursor.getInt(10));
146 item.setApiVersion(cursor.getInt(11));
147 item.setShowCommodityByDefault(cursor.getInt(12) == 1);
148 item.setDefaultCommodity(cursor.getString(13));
149 item.setShowCommentsByDefault(cursor.getInt(14) == 1);
152 .equals(currentProfileUUID))
156 Data.profiles.setValue(list);
159 public static void storeProfilesOrder() {
160 SQLiteDatabase db = App.getDatabase();
161 db.beginTransactionNonExclusive();
164 for (MobileLedgerProfile p : Data.profiles.getValue()) {
165 db.execSQL("update profiles set order_no=? where uuid=?",
166 new Object[]{orderNo, p.getUuid()});
170 db.setTransactionSuccessful();
176 public static ArrayList<LedgerAccount> mergeAccountLists(List<LedgerAccount> oldList,
177 List<LedgerAccount> newList) {
178 LedgerAccount oldAcc, newAcc;
179 ArrayList<LedgerAccount> merged = new ArrayList<>();
181 Iterator<LedgerAccount> oldIterator = oldList.iterator();
182 Iterator<LedgerAccount> newIterator = newList.iterator();
185 if (!oldIterator.hasNext()) {
186 // the rest of the incoming are new
187 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
188 newIterator.forEachRemaining(merged::add);
191 while (newIterator.hasNext())
192 merged.add(newIterator.next());
196 oldAcc = oldIterator.next();
198 if (!newIterator.hasNext()) {
199 // no more incoming accounts. ignore the rest of the old
202 newAcc = newIterator.next();
204 // ignore now missing old items
206 .compareTo(newAcc.getName()) < 0)
209 // add newly found items
211 .compareTo(newAcc.getName()) > 0)
217 // two items with same account names; merge UI-controlled fields
218 oldAcc.setExpanded(newAcc.isExpanded());
219 oldAcc.setAmountsExpanded(newAcc.amountsExpanded());
225 public void mergeAccountList(List<LedgerAccount> newList) {
227 try (LockHolder l = accountsLocker.lockForWriting()) {
228 allAccounts = mergeAccountLists(allAccounts, newList);
229 updateAccountsMap(allAccounts);
232 public LiveData<List<LedgerAccount>> getDisplayedAccounts() {
233 return displayedAccounts;
235 @Contract(value = "null -> false", pure = true)
237 public boolean equals(@Nullable Object obj) {
242 if (obj.getClass() != this.getClass())
245 MobileLedgerProfile p = (MobileLedgerProfile) obj;
246 if (!uuid.equals(p.uuid))
248 if (!name.equals(p.name))
250 if (permitPosting != p.permitPosting)
252 if (showCommentsByDefault != p.showCommentsByDefault)
254 if (showCommodityByDefault != p.showCommodityByDefault)
256 if (!Objects.equals(defaultCommodity, p.defaultCommodity))
258 if (!Objects.equals(preferredAccountsFilter, p.preferredAccountsFilter))
260 if (!Objects.equals(url, p.url))
262 if (authEnabled != p.authEnabled)
264 if (!Objects.equals(authUserName, p.authUserName))
266 if (!Objects.equals(authPassword, p.authPassword))
268 if (themeHue != p.themeHue)
270 if (apiVersion != p.apiVersion)
272 if (!Objects.equals(firstTransactionDate, p.firstTransactionDate))
274 if (!Objects.equals(lastTransactionDate, p.lastTransactionDate))
276 return futureDates == p.futureDates;
278 synchronized public void scheduleAccountListReload() {
279 Logger.debug("async-acc", "scheduleAccountListReload() enter");
280 if ((loader != null) && loader.isAlive()) {
281 Logger.debug("async-acc", "returning early - loader already active");
285 Logger.debug("async-acc", "Starting AccountListLoader");
286 loader = new AccountListLoader(this);
289 synchronized public void abortAccountListReload() {
295 public boolean getShowCommentsByDefault() {
296 return showCommentsByDefault;
298 public void setShowCommentsByDefault(boolean newValue) {
299 this.showCommentsByDefault = newValue;
301 public boolean getShowCommodityByDefault() {
302 return showCommodityByDefault;
304 public void setShowCommodityByDefault(boolean showCommodityByDefault) {
305 this.showCommodityByDefault = showCommodityByDefault;
307 public String getDefaultCommodity() {
308 return defaultCommodity;
310 public void setDefaultCommodity(String defaultCommodity) {
311 this.defaultCommodity = defaultCommodity;
313 public void setDefaultCommodity(CharSequence defaultCommodity) {
314 if (defaultCommodity == null)
315 this.defaultCommodity = null;
317 this.defaultCommodity = String.valueOf(defaultCommodity);
319 public SendTransactionTask.API getApiVersion() {
322 public void setApiVersion(SendTransactionTask.API apiVersion) {
323 this.apiVersion = apiVersion;
325 public void setApiVersion(int apiVersion) {
326 this.apiVersion = SendTransactionTask.API.valueOf(apiVersion);
328 public FutureDates getFutureDates() {
331 public void setFutureDates(int anInt) {
332 futureDates = FutureDates.valueOf(anInt);
334 public void setFutureDates(FutureDates futureDates) {
335 this.futureDates = futureDates;
337 public String getPreferredAccountsFilter() {
338 return preferredAccountsFilter;
340 public void setPreferredAccountsFilter(String preferredAccountsFilter) {
341 this.preferredAccountsFilter = preferredAccountsFilter;
343 public void setPreferredAccountsFilter(CharSequence preferredAccountsFilter) {
344 setPreferredAccountsFilter(String.valueOf(preferredAccountsFilter));
346 public boolean isPostingPermitted() {
347 return permitPosting;
349 public void setPostingPermitted(boolean permitPosting) {
350 this.permitPosting = permitPosting;
352 public String getUuid() {
355 public String getName() {
358 public void setName(CharSequence text) {
359 setName(String.valueOf(text));
361 public void setName(String name) {
364 public String getUrl() {
367 public void setUrl(CharSequence text) {
368 setUrl(String.valueOf(text));
370 public void setUrl(String url) {
373 public boolean isAuthEnabled() {
376 public void setAuthEnabled(boolean authEnabled) {
377 this.authEnabled = authEnabled;
379 public String getAuthUserName() {
382 public void setAuthUserName(CharSequence text) {
383 setAuthUserName(String.valueOf(text));
385 public void setAuthUserName(String authUserName) {
386 this.authUserName = authUserName;
388 public String getAuthPassword() {
391 public void setAuthPassword(CharSequence text) {
392 setAuthPassword(String.valueOf(text));
394 public void setAuthPassword(String authPassword) {
395 this.authPassword = authPassword;
397 public void storeInDB() {
398 SQLiteDatabase db = App.getDatabase();
399 db.beginTransactionNonExclusive();
401 // debug("profiles", String.format("Storing profile in DB: uuid=%s, name=%s, " +
402 // "url=%s, permit_posting=%s, authEnabled=%s, " +
403 // "themeHue=%d", uuid, name, url,
404 // permitPosting ? "TRUE" : "FALSE", authEnabled ? "TRUE" : "FALSE", themeHue));
405 db.execSQL("REPLACE INTO profiles(uuid, name, permit_posting, url, " +
406 "use_authentication, auth_user, auth_password, theme, order_no, " +
407 "preferred_accounts_filter, future_dates, api_version, " +
408 "show_commodity_by_default, default_commodity, show_comments_by_default) " +
409 "VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
410 new Object[]{uuid, name, permitPosting, url, authEnabled,
411 authEnabled ? authUserName : null,
412 authEnabled ? authPassword : null, themeHue, orderNo,
413 preferredAccountsFilter, futureDates.toInt(), apiVersion.toInt(),
414 showCommodityByDefault, defaultCommodity, showCommentsByDefault
416 db.setTransactionSuccessful();
422 public void storeAccount(SQLiteDatabase db, LedgerAccount acc, boolean storeUiFields) {
423 // replace into is a bad idea because it would reset hidden to its default value
424 // we like the default, but for new accounts only
425 String sql = "update accounts set keep = 1";
426 List<Object> params = new ArrayList<>();
428 sql += ", expanded=?";
429 params.add(acc.isExpanded() ? 1 : 0);
431 sql += " where profile=? and name=?";
433 params.add(acc.getName());
434 db.execSQL(sql, params.toArray());
436 db.execSQL("insert into accounts(profile, name, name_upper, parent_name, level, " +
437 "expanded, keep) " + "select ?,?,?,?,?,0,1 where (select changes() = 0)",
438 new Object[]{uuid, acc.getName(), acc.getName().toUpperCase(), acc.getParentName(),
441 // debug("accounts", String.format("Stored account '%s' in DB [%s]", acc.getName(), uuid));
443 public void storeAccountValue(SQLiteDatabase db, String name, String currency, Float amount) {
444 db.execSQL("replace into account_values(profile, account, " +
445 "currency, value, keep) values(?, ?, ?, ?, 1);",
446 new Object[]{uuid, name, Misc.emptyIsNull(currency), amount});
448 public void storeTransaction(SQLiteDatabase db, LedgerTransaction tr) {
450 db.execSQL("DELETE from transactions WHERE profile=? and id=?",
451 new Object[]{uuid, tr.getId()});
452 db.execSQL("DELETE from transaction_accounts WHERE profile = ? and transaction_id=?",
453 new Object[]{uuid, tr.getId()});
455 db.execSQL("INSERT INTO transactions(profile, id, year, month, day, description, " +
456 "comment, data_hash, keep) values(?,?,?,?,?,?,?,?,1)",
457 new Object[]{uuid, tr.getId(), tr.getDate().year, tr.getDate().month,
458 tr.getDate().day, tr.getDescription(), tr.getComment(),
462 for (LedgerTransactionAccount item : tr.getAccounts()) {
463 db.execSQL("INSERT INTO transaction_accounts(profile, transaction_id, " +
464 "account_name, amount, currency, comment) values(?, ?, ?, ?, ?, ?)",
465 new Object[]{uuid, tr.getId(), item.getAccountName(), item.getAmount(),
466 Misc.nullIsEmpty(item.getCurrency()), item.getComment()
469 // debug("profile", String.format("Transaction %d stored", tr.getId()));
471 public String getOption(String name, String default_value) {
472 SQLiteDatabase db = App.getDatabase();
473 try (Cursor cursor = db.rawQuery("select value from options where profile = ? and name=?",
474 new String[]{uuid, name}))
476 if (cursor.moveToFirst()) {
477 String result = cursor.getString(0);
479 if (result == null) {
480 debug("profile", "returning default value for " + name);
481 result = default_value;
484 debug("profile", String.format("option %s=%s", name, result));
489 return default_value;
491 catch (Exception e) {
492 debug("db", "returning default value for " + name, e);
493 return default_value;
496 public long getLongOption(String name, long default_value) {
498 String result = getOption(name, "");
499 if ((result == null) || result.isEmpty()) {
500 debug("profile", String.format("Returning default value for option %s", name));
501 longResult = default_value;
505 longResult = Long.parseLong(result);
506 debug("profile", String.format("option %s=%s", name, result));
508 catch (Exception e) {
509 debug("profile", String.format("Returning default value for option %s", name), e);
510 longResult = default_value;
516 public void setOption(String name, String value) {
517 debug("profile", String.format("setting option %s=%s", name, value));
518 DbOpQueue.add("insert or replace into options(profile, name, value) values(?, ?, ?);",
519 new String[]{uuid, name, value});
521 public void setLongOption(String name, long value) {
522 setOption(name, String.valueOf(value));
524 public void removeFromDB() {
525 SQLiteDatabase db = App.getDatabase();
526 debug("db", String.format("removing profile %s from DB", uuid));
527 db.beginTransactionNonExclusive();
529 Object[] uuid_param = new Object[]{uuid};
530 db.execSQL("delete from profiles where uuid=?", uuid_param);
531 db.execSQL("delete from accounts where profile=?", uuid_param);
532 db.execSQL("delete from account_values where profile=?", uuid_param);
533 db.execSQL("delete from transactions where profile=?", uuid_param);
534 db.execSQL("delete from transaction_accounts where profile=?", uuid_param);
535 db.execSQL("delete from options where profile=?", uuid_param);
536 db.setTransactionSuccessful();
542 public LedgerTransaction loadTransaction(int transactionId) {
543 LedgerTransaction tr = new LedgerTransaction(transactionId, this.uuid);
544 tr.loadData(App.getDatabase());
548 public int getThemeHue() {
549 // debug("profile", String.format("Profile.getThemeHue() returning %d", themeHue));
550 return this.themeHue;
552 public void setThemeHue(Object o) {
553 setThemeId(Integer.parseInt(String.valueOf(o)));
555 public void setThemeId(int themeHue) {
556 // debug("profile", String.format("Profile.setThemeHue(%d) called", themeHue));
557 this.themeHue = themeHue;
559 public void markTransactionsAsNotPresent(SQLiteDatabase db) {
560 db.execSQL("UPDATE transactions set keep=0 where profile=?", new String[]{uuid});
563 private void markAccountsAsNotPresent(SQLiteDatabase db) {
564 db.execSQL("update account_values set keep=0 where profile=?;", new String[]{uuid});
565 db.execSQL("update accounts set keep=0 where profile=?;", new String[]{uuid});
568 private void deleteNotPresentAccounts(SQLiteDatabase db) {
569 db.execSQL("delete from account_values where keep=0 and profile=?", new String[]{uuid});
570 db.execSQL("delete from accounts where keep=0 and profile=?", new String[]{uuid});
572 private void markTransactionAsPresent(SQLiteDatabase db, LedgerTransaction transaction) {
573 db.execSQL("UPDATE transactions SET keep = 1 WHERE profile = ? and id=?",
574 new Object[]{uuid, transaction.getId()
577 private void markTransactionsBeforeTransactionAsPresent(SQLiteDatabase db,
578 LedgerTransaction transaction) {
579 db.execSQL("UPDATE transactions SET keep=1 WHERE profile = ? and id < ?",
580 new Object[]{uuid, transaction.getId()
584 private void deleteNotPresentTransactions(SQLiteDatabase db) {
585 db.execSQL("DELETE FROM transactions WHERE profile=? AND keep = 0", new String[]{uuid});
587 private void setLastUpdateStamp() {
588 debug("db", "Updating transaction value stamp");
589 Date now = new Date();
590 setLongOption(MLDB.OPT_LAST_SCRAPE, now.getTime());
591 Data.lastUpdateDate.postValue(now);
593 public void wipeAllData() {
594 SQLiteDatabase db = App.getDatabase();
595 db.beginTransaction();
597 String[] pUuid = new String[]{uuid};
598 db.execSQL("delete from options where profile=?", pUuid);
599 db.execSQL("delete from accounts where profile=?", pUuid);
600 db.execSQL("delete from account_values where profile=?", pUuid);
601 db.execSQL("delete from transactions where profile=?", pUuid);
602 db.execSQL("delete from transaction_accounts where profile=?", pUuid);
603 db.setTransactionSuccessful();
604 debug("wipe", String.format(Locale.ENGLISH, "Profile %s wiped out", pUuid[0]));
610 public List<Currency> getCurrencies() {
611 SQLiteDatabase db = App.getDatabase();
613 ArrayList<Currency> result = new ArrayList<>();
615 try (Cursor c = db.rawQuery("SELECT c.id, c.name, c.position, c.has_gap FROM currencies c",
618 while (c.moveToNext()) {
619 Currency currency = new Currency(c.getInt(0), c.getString(1),
620 Currency.Position.valueOf(c.getInt(2)), c.getInt(3) == 1);
621 result.add(currency);
627 Currency loadCurrencyByName(String name) {
628 SQLiteDatabase db = App.getDatabase();
629 Currency result = tryLoadCurrencyByName(db, name);
631 throw new RuntimeException(String.format("Unable to load currency '%s'", name));
634 private Currency tryLoadCurrencyByName(SQLiteDatabase db, String name) {
635 try (Cursor cursor = db.rawQuery(
636 "SELECT c.id, c.name, c.position, c.has_gap FROM currencies c WHERE c.name=?",
639 if (cursor.moveToFirst()) {
640 return new Currency(cursor.getInt(0), cursor.getString(1),
641 Currency.Position.valueOf(cursor.getInt(2)), cursor.getInt(3) == 1);
646 public Calendar getFirstTransactionDate() {
647 return firstTransactionDate;
649 public Calendar getLastTransactionDate() {
650 return lastTransactionDate;
652 private void applyTransactionFilter(List<LedgerTransaction> list) {
653 final String accFilter = Data.accountFilter.getValue();
654 if (TextUtils.isEmpty(accFilter)) {
655 displayedTransactions.postValue(list);
658 ArrayList<LedgerTransaction> newList = new ArrayList<>();
659 for (LedgerTransaction tr : list) {
660 if (tr.hasAccountNamedLike(accFilter))
663 displayedTransactions.postValue(newList);
666 synchronized public void storeAccountListAsync(List<LedgerAccount> list,
667 boolean storeUiFields) {
668 if (accountListSaver != null)
669 accountListSaver.interrupt();
670 accountListSaver = new AccountListSaver(this, list, storeUiFields);
671 accountListSaver.start();
673 public void setAndStoreAccountListFromWeb(ArrayList<LedgerAccount> list) {
674 SQLiteDatabase db = App.getDatabase();
675 db.beginTransactionNonExclusive();
677 markAccountsAsNotPresent(db);
678 for (LedgerAccount acc : list) {
679 storeAccount(db, acc, false);
680 for (LedgerAmount amt : acc.getAmounts()) {
681 storeAccountValue(db, acc.getName(), amt.getCurrency(), amt.getAmount());
684 deleteNotPresentAccounts(db);
685 setLastUpdateStamp();
686 db.setTransactionSuccessful();
692 mergeAccountList(list);
693 updateDisplayedAccounts();
695 public synchronized Locker lockAccountsForWriting() {
696 accountsLocker.lockForWriting();
697 return accountsLocker;
699 public void setAndStoreTransactionList(ArrayList<LedgerTransaction> list) {
700 storeTransactionListAsync(this, list);
701 SQLiteDatabase db = App.getDatabase();
702 db.beginTransactionNonExclusive();
704 markTransactionsAsNotPresent(db);
705 for (LedgerTransaction tr : list)
706 storeTransaction(db, tr);
707 deleteNotPresentTransactions(db);
708 setLastUpdateStamp();
709 db.setTransactionSuccessful();
715 allTransactions.postValue(list);
717 private void storeTransactionListAsync(MobileLedgerProfile mobileLedgerProfile,
718 List<LedgerTransaction> list) {
719 if (transactionListSaver != null)
720 transactionListSaver.interrupt();
722 transactionListSaver = new TransactionListSaver(this, list);
723 transactionListSaver.start();
725 public void setAndStoreAccountAndTransactionListFromWeb(List<LedgerAccount> accounts,
726 List<LedgerTransaction> transactions) {
727 storeAccountAndTransactionListAsync(accounts, transactions, false);
729 mergeAccountList(accounts);
730 updateDisplayedAccounts();
732 allTransactions.postValue(transactions);
734 private void storeAccountAndTransactionListAsync(List<LedgerAccount> accounts,
735 List<LedgerTransaction> transactions,
736 boolean storeAccUiFields) {
737 if (accountAndTransactionListSaver != null)
738 accountAndTransactionListSaver.interrupt();
740 accountAndTransactionListSaver =
741 new AccountAndTransactionListSaver(this, accounts, transactions, storeAccUiFields);
742 accountAndTransactionListSaver.start();
744 synchronized public void updateDisplayedAccounts() {
745 if (displayedAccountsUpdater != null) {
746 displayedAccountsUpdater.interrupt();
748 displayedAccountsUpdater = new AccountListDisplayedFilter(this, allAccounts);
749 displayedAccountsUpdater.start();
751 public List<LedgerAccount> getAllAccounts() {
754 private void updateAccountsMap(List<LedgerAccount> newAccounts) {
756 for (LedgerAccount acc : newAccounts) {
757 accountMap.put(acc.getName(), acc);
761 public LedgerAccount locateAccount(String name) {
762 return accountMap.get(name);
765 public enum FutureDates {
766 None(0), OneWeek(7), TwoWeeks(14), OneMonth(30), TwoMonths(60), ThreeMonths(90),
767 SixMonths(180), OneYear(365), All(-1);
768 private static SparseArray<FutureDates> map = new SparseArray<>();
771 for (FutureDates item : FutureDates.values()) {
772 map.put(item.value, item);
777 FutureDates(int value) {
780 public static FutureDates valueOf(int i) {
781 return map.get(i, None);
786 public String getText(Resources resources) {
789 return resources.getString(R.string.future_dates_7);
791 return resources.getString(R.string.future_dates_14);
793 return resources.getString(R.string.future_dates_30);
795 return resources.getString(R.string.future_dates_60);
797 return resources.getString(R.string.future_dates_90);
799 return resources.getString(R.string.future_dates_180);
801 return resources.getString(R.string.future_dates_365);
803 return resources.getString(R.string.future_dates_all);
805 return resources.getString(R.string.future_dates_none);
810 static class AccountListLoader extends Thread {
811 MobileLedgerProfile profile;
812 AccountListLoader(MobileLedgerProfile profile) {
813 this.profile = profile;
817 Logger.debug("async-acc", "AccountListLoader::run() entered");
818 String profileUUID = profile.getUuid();
819 ArrayList<LedgerAccount> list = new ArrayList<>();
820 HashMap<String, LedgerAccount> map = new HashMap<>();
822 String sql = "SELECT a.name, a.expanded, a.amounts_expanded";
823 sql += " from accounts a WHERE a.profile = ?";
824 sql += " ORDER BY a.name";
826 SQLiteDatabase db = App.getDatabase();
827 Logger.debug("async-acc", "AccountListLoader::run() connected to DB");
828 try (Cursor cursor = db.rawQuery(sql, new String[]{profileUUID})) {
829 Logger.debug("async-acc", "AccountListLoader::run() executed query");
830 while (cursor.moveToNext()) {
834 final String accName = cursor.getString(0);
836 // String.format("Read account '%s' from DB [%s]", accName,
838 String parentName = LedgerAccount.extractParentName(accName);
839 LedgerAccount parent;
840 if (parentName != null) {
841 parent = map.get(parentName);
843 throw new IllegalStateException(
844 String.format("Can't load account '%s': parent '%s' not loaded",
845 accName, parentName));
846 parent.setHasSubAccounts(true);
851 LedgerAccount acc = new LedgerAccount(profile, accName, parent);
852 acc.setExpanded(cursor.getInt(1) == 1);
853 acc.setAmountsExpanded(cursor.getInt(2) == 1);
854 acc.setHasSubAccounts(false);
856 try (Cursor c2 = db.rawQuery(
857 "SELECT value, currency FROM account_values WHERE profile = ?" + " " +
858 "AND account = ?", new String[]{profileUUID, accName}))
860 while (c2.moveToNext()) {
861 acc.addAmount(c2.getFloat(0), c2.getString(1));
866 map.put(accName, acc);
868 Logger.debug("async-acc", "AccountListLoader::run() query execution done");
874 Logger.debug("async-acc", "AccountListLoader::run() posting new list");
875 profile.allAccounts = list;
876 profile.updateAccountsMap(list);
877 profile.updateDisplayedAccounts();
881 static class AccountListDisplayedFilter extends Thread {
882 private final MobileLedgerProfile profile;
883 private final List<LedgerAccount> list;
884 AccountListDisplayedFilter(MobileLedgerProfile profile, List<LedgerAccount> list) {
885 this.profile = profile;
890 List<LedgerAccount> newDisplayed = new ArrayList<>();
891 Logger.debug("dFilter", "waiting for synchronized block");
892 Logger.debug("dFilter", String.format(Locale.US,
893 "entered synchronized block (about to examine %d accounts)", list.size()));
894 for (LedgerAccount a : list) {
895 if (isInterrupted()) {
903 if (!isInterrupted()) {
904 profile.displayedAccounts.postValue(newDisplayed);
906 Logger.debug("dFilter", "left synchronized block");
910 private static class AccountListSaver extends Thread {
911 private final MobileLedgerProfile profile;
912 private final List<LedgerAccount> list;
913 private final boolean storeUiFields;
914 AccountListSaver(MobileLedgerProfile profile, List<LedgerAccount> list,
915 boolean storeUiFields) {
917 this.profile = profile;
918 this.storeUiFields = storeUiFields;
922 SQLiteDatabase db = App.getDatabase();
923 db.beginTransactionNonExclusive();
925 profile.markAccountsAsNotPresent(db);
928 for (LedgerAccount acc : list) {
929 profile.storeAccount(db, acc, storeUiFields);
933 profile.deleteNotPresentAccounts(db);
936 profile.setLastUpdateStamp();
937 db.setTransactionSuccessful();
945 private static class TransactionListSaver extends Thread {
946 private final MobileLedgerProfile profile;
947 private final List<LedgerTransaction> list;
948 TransactionListSaver(MobileLedgerProfile profile, List<LedgerTransaction> list) {
950 this.profile = profile;
954 SQLiteDatabase db = App.getDatabase();
955 db.beginTransactionNonExclusive();
957 profile.markTransactionsAsNotPresent(db);
960 for (LedgerTransaction tr : list) {
961 profile.storeTransaction(db, tr);
965 profile.deleteNotPresentTransactions(db);
968 profile.setLastUpdateStamp();
969 db.setTransactionSuccessful();
977 private static class AccountAndTransactionListSaver extends Thread {
978 private final MobileLedgerProfile profile;
979 private final List<LedgerAccount> accounts;
980 private final List<LedgerTransaction> transactions;
981 private final boolean storeAccUiFields;
982 AccountAndTransactionListSaver(MobileLedgerProfile profile, List<LedgerAccount> accounts,
983 List<LedgerTransaction> transactions,
984 boolean storeAccUiFields) {
985 this.accounts = accounts;
986 this.transactions = transactions;
987 this.profile = profile;
988 this.storeAccUiFields = storeAccUiFields;
992 SQLiteDatabase db = App.getDatabase();
993 db.beginTransactionNonExclusive();
995 profile.markAccountsAsNotPresent(db);
999 profile.markTransactionsAsNotPresent(db);
1000 if (isInterrupted()) {
1004 for (LedgerAccount acc : accounts) {
1005 profile.storeAccount(db, acc, storeAccUiFields);
1006 if (isInterrupted())
1010 for (LedgerTransaction tr : transactions) {
1011 profile.storeTransaction(db, tr);
1012 if (isInterrupted()) {
1017 profile.deleteNotPresentAccounts(db);
1018 if (isInterrupted()) {
1021 profile.deleteNotPresentTransactions(db);
1022 if (isInterrupted())
1025 profile.setLastUpdateStamp();
1027 db.setTransactionSuccessful();
1030 db.endTransaction();