2 * Copyright © 2021 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.ui;
20 import android.os.AsyncTask;
21 import android.os.Build;
22 import android.text.TextUtils;
24 import androidx.annotation.Nullable;
25 import androidx.lifecycle.LiveData;
26 import androidx.lifecycle.MutableLiveData;
27 import androidx.lifecycle.ViewModel;
29 import net.ktnx.mobileledger.async.RetrieveTransactionsTask;
30 import net.ktnx.mobileledger.async.TransactionAccumulator;
31 import net.ktnx.mobileledger.async.UpdateTransactionsTask;
32 import net.ktnx.mobileledger.model.AccountListItem;
33 import net.ktnx.mobileledger.model.Data;
34 import net.ktnx.mobileledger.model.LedgerAccount;
35 import net.ktnx.mobileledger.model.LedgerTransaction;
36 import net.ktnx.mobileledger.model.MobileLedgerProfile;
37 import net.ktnx.mobileledger.model.TransactionListItem;
38 import net.ktnx.mobileledger.utils.LockHolder;
39 import net.ktnx.mobileledger.utils.Locker;
40 import net.ktnx.mobileledger.utils.Logger;
41 import net.ktnx.mobileledger.utils.MLDB;
42 import net.ktnx.mobileledger.utils.SimpleDate;
44 import java.util.ArrayList;
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;
52 import static net.ktnx.mobileledger.utils.Logger.debug;
54 public class MainModel extends ViewModel {
55 public final MutableLiveData<Integer> foundTransactionItemIndex = new MutableLiveData<>(null);
56 private final MutableLiveData<Boolean> updatingFlag = new MutableLiveData<>(false);
57 private final MutableLiveData<String> accountFilter = new MutableLiveData<>();
58 private final MutableLiveData<List<TransactionListItem>> displayedTransactions =
59 new MutableLiveData<>(new ArrayList<>());
60 private final MutableLiveData<List<AccountListItem>> displayedAccounts =
61 new MutableLiveData<>();
62 private final Locker accountsLocker = new Locker();
63 private final MutableLiveData<String> updateError = new MutableLiveData<>();
64 private final Map<String, LedgerAccount> accountMap = new HashMap<>();
65 private MobileLedgerProfile profile;
66 private List<LedgerAccount> allAccounts = new ArrayList<>();
67 private SimpleDate firstTransactionDate;
68 private SimpleDate lastTransactionDate;
69 transient private RetrieveTransactionsTask retrieveTransactionsTask;
70 transient private Thread displayedAccountsUpdater;
71 private TransactionsDisplayedFilter displayedTransactionsUpdater;
72 public static ArrayList<LedgerAccount> mergeAccountListsFromWeb(List<LedgerAccount> oldList,
73 List<LedgerAccount> newList) {
74 LedgerAccount oldAcc, newAcc;
75 ArrayList<LedgerAccount> merged = new ArrayList<>();
77 Iterator<LedgerAccount> oldIterator = oldList.iterator();
78 Iterator<LedgerAccount> newIterator = newList.iterator();
81 if (!oldIterator.hasNext()) {
82 // the rest of the incoming are new
83 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
84 newIterator.forEachRemaining(merged::add);
87 while (newIterator.hasNext())
88 merged.add(newIterator.next());
92 oldAcc = oldIterator.next();
94 if (!newIterator.hasNext()) {
95 // no more incoming accounts. ignore the rest of the old
98 newAcc = newIterator.next();
100 // ignore now missing old items
102 .compareTo(newAcc.getName()) < 0)
105 // add newly found items
107 .compareTo(newAcc.getName()) > 0)
113 // two items with same account names; forward-merge UI-controlled fields
114 // it is important that the result list contains a new LedgerAccount instance
115 // so that the change is propagated to the UI
116 newAcc.setExpanded(oldAcc.isExpanded());
117 newAcc.setAmountsExpanded(oldAcc.amountsExpanded());
123 private void setLastUpdateStamp(long transactionCount) {
124 debug("db", "Updating transaction value stamp");
125 Date now = new Date();
126 profile.setLongOption(MLDB.OPT_LAST_SCRAPE, now.getTime());
127 Data.lastUpdateDate.postValue(now);
129 public void scheduleTransactionListReload() {
130 UpdateTransactionsTask task = new UpdateTransactionsTask();
131 task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, this);
133 public LiveData<Boolean> getUpdatingFlag() {
136 public LiveData<String> getUpdateError() {
139 public void setProfile(MobileLedgerProfile profile) {
140 stopTransactionsRetrieval();
141 this.profile = profile;
143 public LiveData<List<TransactionListItem>> getDisplayedTransactions() {
144 return displayedTransactions;
146 public void setDisplayedTransactions(List<TransactionListItem> list, int transactionCount) {
147 displayedTransactions.postValue(list);
148 Data.lastUpdateTransactionCount.postValue(transactionCount);
150 public SimpleDate getFirstTransactionDate() {
151 return firstTransactionDate;
153 public void setFirstTransactionDate(SimpleDate earliestDate) {
154 this.firstTransactionDate = earliestDate;
156 public MutableLiveData<String> getAccountFilter() {
157 return accountFilter;
159 public SimpleDate getLastTransactionDate() {
160 return lastTransactionDate;
162 public void setLastTransactionDate(SimpleDate latestDate) {
163 this.lastTransactionDate = latestDate;
165 private void applyTransactionFilter(List<LedgerTransaction> list) {
166 final String accFilter = accountFilter.getValue();
167 ArrayList<TransactionListItem> newList = new ArrayList<>();
169 TransactionAccumulator accumulator = new TransactionAccumulator(this);
170 if (TextUtils.isEmpty(accFilter))
171 for (LedgerTransaction tr : list)
172 newList.add(new TransactionListItem(tr));
174 for (LedgerTransaction tr : list)
175 if (tr.hasAccountNamedLike(accFilter))
176 newList.add(new TransactionListItem(tr));
178 displayedTransactions.postValue(newList);
180 public synchronized void scheduleTransactionListRetrieval() {
181 if (retrieveTransactionsTask != null) {
182 Logger.debug("db", "Ignoring request for transaction retrieval - already active");
185 MobileLedgerProfile profile = Data.getProfile();
187 retrieveTransactionsTask = new RetrieveTransactionsTask(this, profile, allAccounts);
188 Logger.debug("db", "Created a background transaction retrieval task");
190 retrieveTransactionsTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
192 public synchronized void stopTransactionsRetrieval() {
193 if (retrieveTransactionsTask != null)
194 retrieveTransactionsTask.cancel(true);
196 public void transactionRetrievalDone() {
197 retrieveTransactionsTask = null;
199 public synchronized Locker lockAccountsForWriting() {
200 accountsLocker.lockForWriting();
201 return accountsLocker;
203 public void mergeAccountListFromWeb(List<LedgerAccount> newList) {
205 try (LockHolder l = accountsLocker.lockForWriting()) {
206 allAccounts = mergeAccountListsFromWeb(allAccounts, newList);
207 updateAccountsMap(allAccounts);
210 public LiveData<List<AccountListItem>> getDisplayedAccounts() {
211 return displayedAccounts;
213 public synchronized void setAndStoreAccountAndTransactionListFromWeb(
214 List<LedgerAccount> accounts, List<LedgerTransaction> transactions) {
215 profile.storeAccountAndTransactionListAsync(accounts, transactions);
217 setLastUpdateStamp(transactions.size());
219 mergeAccountListFromWeb(accounts);
220 updateDisplayedAccounts();
222 updateDisplayedTransactionsFromWeb(transactions);
224 synchronized public void updateDisplayedAccounts() {
225 if (displayedAccountsUpdater != null) {
226 displayedAccountsUpdater.interrupt();
228 displayedAccountsUpdater = new AccountListDisplayedFilter(this, allAccounts);
229 displayedAccountsUpdater.start();
231 synchronized public void updateDisplayedTransactionsFromWeb(List<LedgerTransaction> list) {
232 if (displayedTransactionsUpdater != null) {
233 displayedTransactionsUpdater.interrupt();
235 displayedTransactionsUpdater = new TransactionsDisplayedFilter(this, list);
236 displayedTransactionsUpdater.start();
238 public List<LedgerAccount> getAllAccounts() {
241 private void updateAccountsMap(List<LedgerAccount> newAccounts) {
243 for (LedgerAccount acc : newAccounts) {
244 accountMap.put(acc.getName(), acc);
248 public LedgerAccount locateAccount(String name) {
249 return accountMap.get(name);
251 public void clearUpdateError() {
252 updateError.postValue(null);
254 public void clearAccounts() { displayedAccounts.postValue(new ArrayList<>()); }
255 public void clearTransactions() {
256 displayedTransactions.setValue(new ArrayList<>());
258 static class AccountListDisplayedFilter extends Thread {
259 private final MainModel model;
260 private final List<LedgerAccount> list;
261 AccountListDisplayedFilter(MainModel model, List<LedgerAccount> list) {
267 List<AccountListItem> newDisplayed = new ArrayList<>();
268 Logger.debug("dFilter", "waiting for synchronized block");
269 Logger.debug("dFilter", String.format(Locale.US,
270 "entered synchronized block (about to examine %d accounts)", list.size()));
271 newDisplayed.add(new AccountListItem.Header(Data.lastAccountsUpdateText)); // header
274 for (LedgerAccount a : list) {
279 newDisplayed.add(new AccountListItem.Account(a));
283 if (!isInterrupted()) {
284 model.displayedAccounts.postValue(newDisplayed);
285 Data.lastUpdateAccountCount.postValue(count);
287 Logger.debug("dFilter", "left synchronized block");
291 static class TransactionsDisplayedFilter extends Thread {
292 private final MainModel model;
293 private final List<LedgerTransaction> list;
294 TransactionsDisplayedFilter(MainModel model, List<LedgerTransaction> list) {
300 List<LedgerAccount> newDisplayed = new ArrayList<>();
301 Logger.debug("dFilter", "waiting for synchronized block");
302 Logger.debug("dFilter", String.format(Locale.US,
303 "entered synchronized block (about to examine %d transactions)", list.size()));
304 String accNameFilter = model.getAccountFilter()
307 TransactionAccumulator acc = new TransactionAccumulator(model);
308 for (LedgerTransaction tr : list) {
309 if (isInterrupted()) {
313 if (accNameFilter == null || tr.hasAccountNamedLike(accNameFilter)) {
314 acc.put(tr, tr.getDate());
317 if (!isInterrupted()) {
320 Logger.debug("dFilter", "left synchronized block");