]> git.ktnx.net Git - mobile-ledger.git/blob - app/src/main/java/net/ktnx/mobileledger/ui/MainModel.java
adopt Room for displaying account lists
[mobile-ledger.git] / app / src / main / java / net / ktnx / mobileledger / ui / MainModel.java
1 /*
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.
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.ui;
19
20 import android.os.AsyncTask;
21 import android.os.Build;
22 import android.text.TextUtils;
23
24 import androidx.annotation.Nullable;
25 import androidx.lifecycle.LiveData;
26 import androidx.lifecycle.MutableLiveData;
27 import androidx.lifecycle.ViewModel;
28
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;
43
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;
50 import java.util.Map;
51
52 import static net.ktnx.mobileledger.utils.Logger.debug;
53
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<>();
76
77         Iterator<LedgerAccount> oldIterator = oldList.iterator();
78         Iterator<LedgerAccount> newIterator = newList.iterator();
79
80         while (true) {
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);
85                 }
86                 else {
87                     while (newIterator.hasNext())
88                         merged.add(newIterator.next());
89                 }
90                 break;
91             }
92             oldAcc = oldIterator.next();
93
94             if (!newIterator.hasNext()) {
95                 // no more incoming accounts. ignore the rest of the old
96                 break;
97             }
98             newAcc = newIterator.next();
99
100             // ignore now missing old items
101             if (oldAcc.getName()
102                       .compareTo(newAcc.getName()) < 0)
103                 continue;
104
105             // add newly found items
106             if (oldAcc.getName()
107                       .compareTo(newAcc.getName()) > 0)
108             {
109                 merged.add(newAcc);
110                 continue;
111             }
112
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());
118             merged.add(newAcc);
119         }
120
121         return merged;
122     }
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);
128     }
129     public void scheduleTransactionListReload() {
130         UpdateTransactionsTask task = new UpdateTransactionsTask();
131         task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, this);
132     }
133     public LiveData<Boolean> getUpdatingFlag() {
134         return updatingFlag;
135     }
136     public LiveData<String> getUpdateError() {
137         return updateError;
138     }
139     public void setProfile(MobileLedgerProfile profile) {
140         stopTransactionsRetrieval();
141         this.profile = profile;
142     }
143     public LiveData<List<TransactionListItem>> getDisplayedTransactions() {
144         return displayedTransactions;
145     }
146     public void setDisplayedTransactions(List<TransactionListItem> list, int transactionCount) {
147         displayedTransactions.postValue(list);
148         Data.lastUpdateTransactionCount.postValue(transactionCount);
149     }
150     public SimpleDate getFirstTransactionDate() {
151         return firstTransactionDate;
152     }
153     public void setFirstTransactionDate(SimpleDate earliestDate) {
154         this.firstTransactionDate = earliestDate;
155     }
156     public MutableLiveData<String> getAccountFilter() {
157         return accountFilter;
158     }
159     public SimpleDate getLastTransactionDate() {
160         return lastTransactionDate;
161     }
162     public void setLastTransactionDate(SimpleDate latestDate) {
163         this.lastTransactionDate = latestDate;
164     }
165     private void applyTransactionFilter(List<LedgerTransaction> list) {
166         final String accFilter = accountFilter.getValue();
167         ArrayList<TransactionListItem> newList = new ArrayList<>();
168
169         TransactionAccumulator accumulator = new TransactionAccumulator(this);
170         if (TextUtils.isEmpty(accFilter))
171             for (LedgerTransaction tr : list)
172                 newList.add(new TransactionListItem(tr));
173         else
174             for (LedgerTransaction tr : list)
175                 if (tr.hasAccountNamedLike(accFilter))
176                     newList.add(new TransactionListItem(tr));
177
178         displayedTransactions.postValue(newList);
179     }
180     public synchronized void scheduleTransactionListRetrieval() {
181         if (retrieveTransactionsTask != null) {
182             Logger.debug("db", "Ignoring request for transaction retrieval - already active");
183             return;
184         }
185         MobileLedgerProfile profile = Data.getProfile();
186
187         retrieveTransactionsTask = new RetrieveTransactionsTask(this, profile, allAccounts);
188         Logger.debug("db", "Created a background transaction retrieval task");
189
190         retrieveTransactionsTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
191     }
192     public synchronized void stopTransactionsRetrieval() {
193         if (retrieveTransactionsTask != null)
194             retrieveTransactionsTask.cancel(true);
195     }
196     public void transactionRetrievalDone() {
197         retrieveTransactionsTask = null;
198     }
199     public synchronized Locker lockAccountsForWriting() {
200         accountsLocker.lockForWriting();
201         return accountsLocker;
202     }
203     public void mergeAccountListFromWeb(List<LedgerAccount> newList) {
204
205         try (LockHolder l = accountsLocker.lockForWriting()) {
206             allAccounts = mergeAccountListsFromWeb(allAccounts, newList);
207             updateAccountsMap(allAccounts);
208         }
209     }
210     public LiveData<List<AccountListItem>> getDisplayedAccounts() {
211         return displayedAccounts;
212     }
213     public synchronized void setAndStoreAccountAndTransactionListFromWeb(
214             List<LedgerAccount> accounts, List<LedgerTransaction> transactions) {
215         profile.storeAccountAndTransactionListAsync(accounts, transactions);
216
217         setLastUpdateStamp(transactions.size());
218
219         mergeAccountListFromWeb(accounts);
220         updateDisplayedAccounts();
221
222         updateDisplayedTransactionsFromWeb(transactions);
223     }
224     synchronized public void updateDisplayedAccounts() {
225         if (displayedAccountsUpdater != null) {
226             displayedAccountsUpdater.interrupt();
227         }
228         displayedAccountsUpdater = new AccountListDisplayedFilter(this, allAccounts);
229         displayedAccountsUpdater.start();
230     }
231     synchronized public void updateDisplayedTransactionsFromWeb(List<LedgerTransaction> list) {
232         if (displayedTransactionsUpdater != null) {
233             displayedTransactionsUpdater.interrupt();
234         }
235         displayedTransactionsUpdater = new TransactionsDisplayedFilter(this, list);
236         displayedTransactionsUpdater.start();
237     }
238     public List<LedgerAccount> getAllAccounts() {
239         return allAccounts;
240     }
241     private void updateAccountsMap(List<LedgerAccount> newAccounts) {
242         accountMap.clear();
243         for (LedgerAccount acc : newAccounts) {
244             accountMap.put(acc.getName(), acc);
245         }
246     }
247     @Nullable
248     public LedgerAccount locateAccount(String name) {
249         return accountMap.get(name);
250     }
251     public void clearUpdateError() {
252         updateError.postValue(null);
253     }
254     public void clearAccounts() { displayedAccounts.postValue(new ArrayList<>()); }
255     public void clearTransactions() {
256         displayedTransactions.setValue(new ArrayList<>());
257     }
258     static class AccountListDisplayedFilter extends Thread {
259         private final MainModel model;
260         private final List<LedgerAccount> list;
261         AccountListDisplayedFilter(MainModel model, List<LedgerAccount> list) {
262             this.model = model;
263             this.list = list;
264         }
265         @Override
266         public void run() {
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
272
273             int count = 0;
274             for (LedgerAccount a : list) {
275                 if (isInterrupted())
276                     return;
277
278                 if (a.isVisible()) {
279                     newDisplayed.add(new AccountListItem.Account(a));
280                     count++;
281                 }
282             }
283             if (!isInterrupted()) {
284                 model.displayedAccounts.postValue(newDisplayed);
285                 Data.lastUpdateAccountCount.postValue(count);
286             }
287             Logger.debug("dFilter", "left synchronized block");
288         }
289     }
290
291     static class TransactionsDisplayedFilter extends Thread {
292         private final MainModel model;
293         private final List<LedgerTransaction> list;
294         TransactionsDisplayedFilter(MainModel model, List<LedgerTransaction> list) {
295             this.model = model;
296             this.list = list;
297         }
298         @Override
299         public void run() {
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()
305                                         .getValue();
306
307             TransactionAccumulator acc = new TransactionAccumulator(model);
308             for (LedgerTransaction tr : list) {
309                 if (isInterrupted()) {
310                     return;
311                 }
312
313                 if (accNameFilter == null || tr.hasAccountNamedLike(accNameFilter)) {
314                     acc.put(tr, tr.getDate());
315                 }
316             }
317             if (!isInterrupted()) {
318                 acc.done();
319             }
320             Logger.debug("dFilter", "left synchronized block");
321         }
322     }
323 }