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> mergeAccountListsFromWeb(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; forward-merge UI-controlled fields
218 // it is important that the result list contains a new LedgerAccount instance
219 // so that the change is propagated to the UI
220 newAcc.setExpanded(oldAcc.isExpanded());
221 newAcc.setAmountsExpanded(oldAcc.amountsExpanded());
227 public void mergeAccountListFromWeb(List<LedgerAccount> newList) {
229 try (LockHolder l = accountsLocker.lockForWriting()) {
230 allAccounts = mergeAccountListsFromWeb(allAccounts, newList);
231 updateAccountsMap(allAccounts);
234 public LiveData<List<LedgerAccount>> getDisplayedAccounts() {
235 return displayedAccounts;
237 @Contract(value = "null -> false", pure = true)
239 public boolean equals(@Nullable Object obj) {
244 if (obj.getClass() != this.getClass())
247 MobileLedgerProfile p = (MobileLedgerProfile) obj;
248 if (!uuid.equals(p.uuid))
250 if (!name.equals(p.name))
252 if (permitPosting != p.permitPosting)
254 if (showCommentsByDefault != p.showCommentsByDefault)
256 if (showCommodityByDefault != p.showCommodityByDefault)
258 if (!Objects.equals(defaultCommodity, p.defaultCommodity))
260 if (!Objects.equals(preferredAccountsFilter, p.preferredAccountsFilter))
262 if (!Objects.equals(url, p.url))
264 if (authEnabled != p.authEnabled)
266 if (!Objects.equals(authUserName, p.authUserName))
268 if (!Objects.equals(authPassword, p.authPassword))
270 if (themeHue != p.themeHue)
272 if (apiVersion != p.apiVersion)
274 if (!Objects.equals(firstTransactionDate, p.firstTransactionDate))
276 if (!Objects.equals(lastTransactionDate, p.lastTransactionDate))
278 return futureDates == p.futureDates;
280 synchronized public void scheduleAccountListReload() {
281 Logger.debug("async-acc", "scheduleAccountListReload() enter");
282 if ((loader != null) && loader.isAlive()) {
283 Logger.debug("async-acc", "returning early - loader already active");
287 Logger.debug("async-acc", "Starting AccountListLoader");
288 loader = new AccountListLoader(this);
291 synchronized public void abortAccountListReload() {
297 public boolean getShowCommentsByDefault() {
298 return showCommentsByDefault;
300 public void setShowCommentsByDefault(boolean newValue) {
301 this.showCommentsByDefault = newValue;
303 public boolean getShowCommodityByDefault() {
304 return showCommodityByDefault;
306 public void setShowCommodityByDefault(boolean showCommodityByDefault) {
307 this.showCommodityByDefault = showCommodityByDefault;
309 public String getDefaultCommodity() {
310 return defaultCommodity;
312 public void setDefaultCommodity(String defaultCommodity) {
313 this.defaultCommodity = defaultCommodity;
315 public void setDefaultCommodity(CharSequence defaultCommodity) {
316 if (defaultCommodity == null)
317 this.defaultCommodity = null;
319 this.defaultCommodity = String.valueOf(defaultCommodity);
321 public SendTransactionTask.API getApiVersion() {
324 public void setApiVersion(SendTransactionTask.API apiVersion) {
325 this.apiVersion = apiVersion;
327 public void setApiVersion(int apiVersion) {
328 this.apiVersion = SendTransactionTask.API.valueOf(apiVersion);
330 public FutureDates getFutureDates() {
333 public void setFutureDates(int anInt) {
334 futureDates = FutureDates.valueOf(anInt);
336 public void setFutureDates(FutureDates futureDates) {
337 this.futureDates = futureDates;
339 public String getPreferredAccountsFilter() {
340 return preferredAccountsFilter;
342 public void setPreferredAccountsFilter(String preferredAccountsFilter) {
343 this.preferredAccountsFilter = preferredAccountsFilter;
345 public void setPreferredAccountsFilter(CharSequence preferredAccountsFilter) {
346 setPreferredAccountsFilter(String.valueOf(preferredAccountsFilter));
348 public boolean isPostingPermitted() {
349 return permitPosting;
351 public void setPostingPermitted(boolean permitPosting) {
352 this.permitPosting = permitPosting;
354 public String getUuid() {
357 public String getName() {
360 public void setName(CharSequence text) {
361 setName(String.valueOf(text));
363 public void setName(String name) {
366 public String getUrl() {
369 public void setUrl(CharSequence text) {
370 setUrl(String.valueOf(text));
372 public void setUrl(String url) {
375 public boolean isAuthEnabled() {
378 public void setAuthEnabled(boolean authEnabled) {
379 this.authEnabled = authEnabled;
381 public String getAuthUserName() {
384 public void setAuthUserName(CharSequence text) {
385 setAuthUserName(String.valueOf(text));
387 public void setAuthUserName(String authUserName) {
388 this.authUserName = authUserName;
390 public String getAuthPassword() {
393 public void setAuthPassword(CharSequence text) {
394 setAuthPassword(String.valueOf(text));
396 public void setAuthPassword(String authPassword) {
397 this.authPassword = authPassword;
399 public void storeInDB() {
400 SQLiteDatabase db = App.getDatabase();
401 db.beginTransactionNonExclusive();
403 // debug("profiles", String.format("Storing profile in DB: uuid=%s, name=%s, " +
404 // "url=%s, permit_posting=%s, authEnabled=%s, " +
405 // "themeHue=%d", uuid, name, url,
406 // permitPosting ? "TRUE" : "FALSE", authEnabled ? "TRUE" : "FALSE", themeHue));
407 db.execSQL("REPLACE INTO profiles(uuid, name, permit_posting, url, " +
408 "use_authentication, auth_user, auth_password, theme, order_no, " +
409 "preferred_accounts_filter, future_dates, api_version, " +
410 "show_commodity_by_default, default_commodity, show_comments_by_default) " +
411 "VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
412 new Object[]{uuid, name, permitPosting, url, authEnabled,
413 authEnabled ? authUserName : null,
414 authEnabled ? authPassword : null, themeHue, orderNo,
415 preferredAccountsFilter, futureDates.toInt(), apiVersion.toInt(),
416 showCommodityByDefault, defaultCommodity, showCommentsByDefault
418 db.setTransactionSuccessful();
424 public void storeAccount(SQLiteDatabase db, LedgerAccount acc, boolean storeUiFields) {
425 // replace into is a bad idea because it would reset hidden to its default value
426 // we like the default, but for new accounts only
427 String sql = "update accounts set keep = 1";
428 List<Object> params = new ArrayList<>();
430 sql += ", expanded=?";
431 params.add(acc.isExpanded() ? 1 : 0);
433 sql += " where profile=? and name=?";
435 params.add(acc.getName());
436 db.execSQL(sql, params.toArray());
438 db.execSQL("insert into accounts(profile, name, name_upper, parent_name, level, " +
439 "expanded, keep) " + "select ?,?,?,?,?,0,1 where (select changes() = 0)",
440 new Object[]{uuid, acc.getName(), acc.getName().toUpperCase(), acc.getParentName(),
443 // debug("accounts", String.format("Stored account '%s' in DB [%s]", acc.getName(), uuid));
445 public void storeAccountValue(SQLiteDatabase db, String name, String currency, Float amount) {
446 db.execSQL("replace into account_values(profile, account, " +
447 "currency, value, keep) values(?, ?, ?, ?, 1);",
448 new Object[]{uuid, name, Misc.emptyIsNull(currency), amount});
450 public void storeTransaction(SQLiteDatabase db, LedgerTransaction tr) {
452 db.execSQL("DELETE from transactions WHERE profile=? and id=?",
453 new Object[]{uuid, tr.getId()});
454 db.execSQL("DELETE from transaction_accounts WHERE profile = ? and transaction_id=?",
455 new Object[]{uuid, tr.getId()});
457 db.execSQL("INSERT INTO transactions(profile, id, year, month, day, description, " +
458 "comment, data_hash, keep) values(?,?,?,?,?,?,?,?,1)",
459 new Object[]{uuid, tr.getId(), tr.getDate().year, tr.getDate().month,
460 tr.getDate().day, tr.getDescription(), tr.getComment(),
464 for (LedgerTransactionAccount item : tr.getAccounts()) {
465 db.execSQL("INSERT INTO transaction_accounts(profile, transaction_id, " +
466 "account_name, amount, currency, comment) values(?, ?, ?, ?, ?, ?)",
467 new Object[]{uuid, tr.getId(), item.getAccountName(), item.getAmount(),
468 Misc.nullIsEmpty(item.getCurrency()), item.getComment()
471 // debug("profile", String.format("Transaction %d stored", tr.getId()));
473 public String getOption(String name, String default_value) {
474 SQLiteDatabase db = App.getDatabase();
475 try (Cursor cursor = db.rawQuery("select value from options where profile = ? and name=?",
476 new String[]{uuid, name}))
478 if (cursor.moveToFirst()) {
479 String result = cursor.getString(0);
481 if (result == null) {
482 debug("profile", "returning default value for " + name);
483 result = default_value;
486 debug("profile", String.format("option %s=%s", name, result));
491 return default_value;
493 catch (Exception e) {
494 debug("db", "returning default value for " + name, e);
495 return default_value;
498 public long getLongOption(String name, long default_value) {
500 String result = getOption(name, "");
501 if ((result == null) || result.isEmpty()) {
502 debug("profile", String.format("Returning default value for option %s", name));
503 longResult = default_value;
507 longResult = Long.parseLong(result);
508 debug("profile", String.format("option %s=%s", name, result));
510 catch (Exception e) {
511 debug("profile", String.format("Returning default value for option %s", name), e);
512 longResult = default_value;
518 public void setOption(String name, String value) {
519 debug("profile", String.format("setting option %s=%s", name, value));
520 DbOpQueue.add("insert or replace into options(profile, name, value) values(?, ?, ?);",
521 new String[]{uuid, name, value});
523 public void setLongOption(String name, long value) {
524 setOption(name, String.valueOf(value));
526 public void removeFromDB() {
527 SQLiteDatabase db = App.getDatabase();
528 debug("db", String.format("removing profile %s from DB", uuid));
529 db.beginTransactionNonExclusive();
531 Object[] uuid_param = new Object[]{uuid};
532 db.execSQL("delete from profiles where uuid=?", uuid_param);
533 db.execSQL("delete from accounts where profile=?", uuid_param);
534 db.execSQL("delete from account_values where profile=?", uuid_param);
535 db.execSQL("delete from transactions where profile=?", uuid_param);
536 db.execSQL("delete from transaction_accounts where profile=?", uuid_param);
537 db.execSQL("delete from options where profile=?", uuid_param);
538 db.setTransactionSuccessful();
544 public LedgerTransaction loadTransaction(int transactionId) {
545 LedgerTransaction tr = new LedgerTransaction(transactionId, this.uuid);
546 tr.loadData(App.getDatabase());
550 public int getThemeHue() {
551 // debug("profile", String.format("Profile.getThemeHue() returning %d", themeHue));
552 return this.themeHue;
554 public void setThemeHue(Object o) {
555 setThemeId(Integer.parseInt(String.valueOf(o)));
557 public void setThemeId(int themeHue) {
558 // debug("profile", String.format("Profile.setThemeHue(%d) called", themeHue));
559 this.themeHue = themeHue;
561 public void markTransactionsAsNotPresent(SQLiteDatabase db) {
562 db.execSQL("UPDATE transactions set keep=0 where profile=?", new String[]{uuid});
565 private void markAccountsAsNotPresent(SQLiteDatabase db) {
566 db.execSQL("update account_values set keep=0 where profile=?;", new String[]{uuid});
567 db.execSQL("update accounts set keep=0 where profile=?;", new String[]{uuid});
570 private void deleteNotPresentAccounts(SQLiteDatabase db) {
571 db.execSQL("delete from account_values where keep=0 and profile=?", new String[]{uuid});
572 db.execSQL("delete from accounts where keep=0 and profile=?", new String[]{uuid});
574 private void markTransactionAsPresent(SQLiteDatabase db, LedgerTransaction transaction) {
575 db.execSQL("UPDATE transactions SET keep = 1 WHERE profile = ? and id=?",
576 new Object[]{uuid, transaction.getId()
579 private void markTransactionsBeforeTransactionAsPresent(SQLiteDatabase db,
580 LedgerTransaction transaction) {
581 db.execSQL("UPDATE transactions SET keep=1 WHERE profile = ? and id < ?",
582 new Object[]{uuid, transaction.getId()
586 private void deleteNotPresentTransactions(SQLiteDatabase db) {
587 db.execSQL("DELETE FROM transactions WHERE profile=? AND keep = 0", new String[]{uuid});
589 private void setLastUpdateStamp() {
590 debug("db", "Updating transaction value stamp");
591 Date now = new Date();
592 setLongOption(MLDB.OPT_LAST_SCRAPE, now.getTime());
593 Data.lastUpdateDate.postValue(now);
595 public void wipeAllData() {
596 SQLiteDatabase db = App.getDatabase();
597 db.beginTransaction();
599 String[] pUuid = new String[]{uuid};
600 db.execSQL("delete from options where profile=?", pUuid);
601 db.execSQL("delete from accounts where profile=?", pUuid);
602 db.execSQL("delete from account_values where profile=?", pUuid);
603 db.execSQL("delete from transactions where profile=?", pUuid);
604 db.execSQL("delete from transaction_accounts where profile=?", pUuid);
605 db.setTransactionSuccessful();
606 debug("wipe", String.format(Locale.ENGLISH, "Profile %s wiped out", pUuid[0]));
612 public List<Currency> getCurrencies() {
613 SQLiteDatabase db = App.getDatabase();
615 ArrayList<Currency> result = new ArrayList<>();
617 try (Cursor c = db.rawQuery("SELECT c.id, c.name, c.position, c.has_gap FROM currencies c",
620 while (c.moveToNext()) {
621 Currency currency = new Currency(c.getInt(0), c.getString(1),
622 Currency.Position.valueOf(c.getInt(2)), c.getInt(3) == 1);
623 result.add(currency);
629 Currency loadCurrencyByName(String name) {
630 SQLiteDatabase db = App.getDatabase();
631 Currency result = tryLoadCurrencyByName(db, name);
633 throw new RuntimeException(String.format("Unable to load currency '%s'", name));
636 private Currency tryLoadCurrencyByName(SQLiteDatabase db, String name) {
637 try (Cursor cursor = db.rawQuery(
638 "SELECT c.id, c.name, c.position, c.has_gap FROM currencies c WHERE c.name=?",
641 if (cursor.moveToFirst()) {
642 return new Currency(cursor.getInt(0), cursor.getString(1),
643 Currency.Position.valueOf(cursor.getInt(2)), cursor.getInt(3) == 1);
648 public Calendar getFirstTransactionDate() {
649 return firstTransactionDate;
651 public Calendar getLastTransactionDate() {
652 return lastTransactionDate;
654 private void applyTransactionFilter(List<LedgerTransaction> list) {
655 final String accFilter = Data.accountFilter.getValue();
656 if (TextUtils.isEmpty(accFilter)) {
657 displayedTransactions.postValue(list);
660 ArrayList<LedgerTransaction> newList = new ArrayList<>();
661 for (LedgerTransaction tr : list) {
662 if (tr.hasAccountNamedLike(accFilter))
665 displayedTransactions.postValue(newList);
668 synchronized public void storeAccountListAsync(List<LedgerAccount> list,
669 boolean storeUiFields) {
670 if (accountListSaver != null)
671 accountListSaver.interrupt();
672 accountListSaver = new AccountListSaver(this, list, storeUiFields);
673 accountListSaver.start();
675 public void setAndStoreAccountListFromWeb(ArrayList<LedgerAccount> list) {
676 SQLiteDatabase db = App.getDatabase();
677 db.beginTransactionNonExclusive();
679 markAccountsAsNotPresent(db);
680 for (LedgerAccount acc : list) {
681 storeAccount(db, acc, false);
682 for (LedgerAmount amt : acc.getAmounts()) {
683 storeAccountValue(db, acc.getName(), amt.getCurrency(), amt.getAmount());
686 deleteNotPresentAccounts(db);
687 setLastUpdateStamp();
688 db.setTransactionSuccessful();
694 mergeAccountListFromWeb(list);
695 updateDisplayedAccounts();
697 public synchronized Locker lockAccountsForWriting() {
698 accountsLocker.lockForWriting();
699 return accountsLocker;
701 public void setAndStoreTransactionList(ArrayList<LedgerTransaction> list) {
702 storeTransactionListAsync(this, list);
703 SQLiteDatabase db = App.getDatabase();
704 db.beginTransactionNonExclusive();
706 markTransactionsAsNotPresent(db);
707 for (LedgerTransaction tr : list)
708 storeTransaction(db, tr);
709 deleteNotPresentTransactions(db);
710 setLastUpdateStamp();
711 db.setTransactionSuccessful();
717 allTransactions.postValue(list);
719 private void storeTransactionListAsync(MobileLedgerProfile mobileLedgerProfile,
720 List<LedgerTransaction> list) {
721 if (transactionListSaver != null)
722 transactionListSaver.interrupt();
724 transactionListSaver = new TransactionListSaver(this, list);
725 transactionListSaver.start();
727 public void setAndStoreAccountAndTransactionListFromWeb(List<LedgerAccount> accounts,
728 List<LedgerTransaction> transactions) {
729 storeAccountAndTransactionListAsync(accounts, transactions, false);
731 mergeAccountListFromWeb(accounts);
732 updateDisplayedAccounts();
734 allTransactions.postValue(transactions);
736 private void storeAccountAndTransactionListAsync(List<LedgerAccount> accounts,
737 List<LedgerTransaction> transactions,
738 boolean storeAccUiFields) {
739 if (accountAndTransactionListSaver != null)
740 accountAndTransactionListSaver.interrupt();
742 accountAndTransactionListSaver =
743 new AccountAndTransactionListSaver(this, accounts, transactions, storeAccUiFields);
744 accountAndTransactionListSaver.start();
746 synchronized public void updateDisplayedAccounts() {
747 if (displayedAccountsUpdater != null) {
748 displayedAccountsUpdater.interrupt();
750 displayedAccountsUpdater = new AccountListDisplayedFilter(this, allAccounts);
751 displayedAccountsUpdater.start();
753 public List<LedgerAccount> getAllAccounts() {
756 private void updateAccountsMap(List<LedgerAccount> newAccounts) {
758 for (LedgerAccount acc : newAccounts) {
759 accountMap.put(acc.getName(), acc);
763 public LedgerAccount locateAccount(String name) {
764 return accountMap.get(name);
767 public enum FutureDates {
768 None(0), OneWeek(7), TwoWeeks(14), OneMonth(30), TwoMonths(60), ThreeMonths(90),
769 SixMonths(180), OneYear(365), All(-1);
770 private static SparseArray<FutureDates> map = new SparseArray<>();
773 for (FutureDates item : FutureDates.values()) {
774 map.put(item.value, item);
779 FutureDates(int value) {
782 public static FutureDates valueOf(int i) {
783 return map.get(i, None);
788 public String getText(Resources resources) {
791 return resources.getString(R.string.future_dates_7);
793 return resources.getString(R.string.future_dates_14);
795 return resources.getString(R.string.future_dates_30);
797 return resources.getString(R.string.future_dates_60);
799 return resources.getString(R.string.future_dates_90);
801 return resources.getString(R.string.future_dates_180);
803 return resources.getString(R.string.future_dates_365);
805 return resources.getString(R.string.future_dates_all);
807 return resources.getString(R.string.future_dates_none);
812 static class AccountListLoader extends Thread {
813 MobileLedgerProfile profile;
814 AccountListLoader(MobileLedgerProfile profile) {
815 this.profile = profile;
819 Logger.debug("async-acc", "AccountListLoader::run() entered");
820 String profileUUID = profile.getUuid();
821 ArrayList<LedgerAccount> list = new ArrayList<>();
822 HashMap<String, LedgerAccount> map = new HashMap<>();
824 String sql = "SELECT a.name, a.expanded, a.amounts_expanded";
825 sql += " from accounts a WHERE a.profile = ?";
826 sql += " ORDER BY a.name";
828 SQLiteDatabase db = App.getDatabase();
829 Logger.debug("async-acc", "AccountListLoader::run() connected to DB");
830 try (Cursor cursor = db.rawQuery(sql, new String[]{profileUUID})) {
831 Logger.debug("async-acc", "AccountListLoader::run() executed query");
832 while (cursor.moveToNext()) {
836 final String accName = cursor.getString(0);
838 // String.format("Read account '%s' from DB [%s]", accName,
840 String parentName = LedgerAccount.extractParentName(accName);
841 LedgerAccount parent;
842 if (parentName != null) {
843 parent = map.get(parentName);
845 throw new IllegalStateException(
846 String.format("Can't load account '%s': parent '%s' not loaded",
847 accName, parentName));
848 parent.setHasSubAccounts(true);
853 LedgerAccount acc = new LedgerAccount(profile, accName, parent);
854 acc.setExpanded(cursor.getInt(1) == 1);
855 acc.setAmountsExpanded(cursor.getInt(2) == 1);
856 acc.setHasSubAccounts(false);
858 try (Cursor c2 = db.rawQuery(
859 "SELECT value, currency FROM account_values WHERE profile = ?" + " " +
860 "AND account = ?", new String[]{profileUUID, accName}))
862 while (c2.moveToNext()) {
863 acc.addAmount(c2.getFloat(0), c2.getString(1));
868 map.put(accName, acc);
870 Logger.debug("async-acc", "AccountListLoader::run() query execution done");
876 Logger.debug("async-acc", "AccountListLoader::run() posting new list");
877 profile.allAccounts = list;
878 profile.updateAccountsMap(list);
879 profile.updateDisplayedAccounts();
883 static class AccountListDisplayedFilter extends Thread {
884 private final MobileLedgerProfile profile;
885 private final List<LedgerAccount> list;
886 AccountListDisplayedFilter(MobileLedgerProfile profile, List<LedgerAccount> list) {
887 this.profile = profile;
892 List<LedgerAccount> newDisplayed = new ArrayList<>();
893 Logger.debug("dFilter", "waiting for synchronized block");
894 Logger.debug("dFilter", String.format(Locale.US,
895 "entered synchronized block (about to examine %d accounts)", list.size()));
896 for (LedgerAccount a : list) {
897 if (isInterrupted()) {
905 if (!isInterrupted()) {
906 profile.displayedAccounts.postValue(newDisplayed);
908 Logger.debug("dFilter", "left synchronized block");
912 private static class AccountListSaver extends Thread {
913 private final MobileLedgerProfile profile;
914 private final List<LedgerAccount> list;
915 private final boolean storeUiFields;
916 AccountListSaver(MobileLedgerProfile profile, List<LedgerAccount> list,
917 boolean storeUiFields) {
919 this.profile = profile;
920 this.storeUiFields = storeUiFields;
924 SQLiteDatabase db = App.getDatabase();
925 db.beginTransactionNonExclusive();
927 profile.markAccountsAsNotPresent(db);
930 for (LedgerAccount acc : list) {
931 profile.storeAccount(db, acc, storeUiFields);
935 profile.deleteNotPresentAccounts(db);
938 profile.setLastUpdateStamp();
939 db.setTransactionSuccessful();
947 private static class TransactionListSaver extends Thread {
948 private final MobileLedgerProfile profile;
949 private final List<LedgerTransaction> list;
950 TransactionListSaver(MobileLedgerProfile profile, List<LedgerTransaction> list) {
952 this.profile = profile;
956 SQLiteDatabase db = App.getDatabase();
957 db.beginTransactionNonExclusive();
959 profile.markTransactionsAsNotPresent(db);
962 for (LedgerTransaction tr : list) {
963 profile.storeTransaction(db, tr);
967 profile.deleteNotPresentTransactions(db);
970 profile.setLastUpdateStamp();
971 db.setTransactionSuccessful();
979 private static class AccountAndTransactionListSaver extends Thread {
980 private final MobileLedgerProfile profile;
981 private final List<LedgerAccount> accounts;
982 private final List<LedgerTransaction> transactions;
983 private final boolean storeAccUiFields;
984 AccountAndTransactionListSaver(MobileLedgerProfile profile, List<LedgerAccount> accounts,
985 List<LedgerTransaction> transactions,
986 boolean storeAccUiFields) {
987 this.accounts = accounts;
988 this.transactions = transactions;
989 this.profile = profile;
990 this.storeAccUiFields = storeAccUiFields;
994 SQLiteDatabase db = App.getDatabase();
995 db.beginTransactionNonExclusive();
997 profile.markAccountsAsNotPresent(db);
1001 profile.markTransactionsAsNotPresent(db);
1002 if (isInterrupted()) {
1006 for (LedgerAccount acc : accounts) {
1007 profile.storeAccount(db, acc, storeAccUiFields);
1008 if (isInterrupted())
1012 for (LedgerTransaction tr : transactions) {
1013 profile.storeTransaction(db, tr);
1014 if (isInterrupted()) {
1019 profile.deleteNotPresentAccounts(db);
1020 if (isInterrupted()) {
1023 profile.deleteNotPresentTransactions(db);
1024 if (isInterrupted())
1027 profile.setLastUpdateStamp();
1029 db.setTransactionSuccessful();
1032 db.endTransaction();