]> git.ktnx.net Git - mobile-ledger.git/blob - app/src/main/java/net/ktnx/mobileledger/ui/activity/NewTransactionItemsAdapter.java
e25b9a10d01b66885af1ec0c2eb90c9631cab82c
[mobile-ledger.git] / app / src / main / java / net / ktnx / mobileledger / ui / activity / NewTransactionItemsAdapter.java
1 /*
2  * Copyright © 2019 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.activity;
19
20 import android.annotation.SuppressLint;
21 import android.database.Cursor;
22 import android.view.LayoutInflater;
23 import android.view.ViewGroup;
24 import android.widget.LinearLayout;
25
26 import androidx.annotation.NonNull;
27 import androidx.annotation.Nullable;
28 import androidx.recyclerview.widget.ItemTouchHelper;
29 import androidx.recyclerview.widget.RecyclerView;
30
31 import net.ktnx.mobileledger.App;
32 import net.ktnx.mobileledger.BuildConfig;
33 import net.ktnx.mobileledger.R;
34 import net.ktnx.mobileledger.async.DescriptionSelectedCallback;
35 import net.ktnx.mobileledger.model.Currency;
36 import net.ktnx.mobileledger.model.Data;
37 import net.ktnx.mobileledger.model.LedgerTransaction;
38 import net.ktnx.mobileledger.model.LedgerTransactionAccount;
39 import net.ktnx.mobileledger.model.MobileLedgerProfile;
40 import net.ktnx.mobileledger.utils.Logger;
41 import net.ktnx.mobileledger.utils.Misc;
42
43 import java.util.ArrayList;
44 import java.util.HashMap;
45 import java.util.List;
46 import java.util.Locale;
47 import java.util.Set;
48
49 import static net.ktnx.mobileledger.utils.Logger.debug;
50
51 class NewTransactionItemsAdapter extends RecyclerView.Adapter<NewTransactionItemHolder>
52         implements DescriptionSelectedCallback {
53     NewTransactionModel model;
54     private MobileLedgerProfile mProfile;
55     private ItemTouchHelper touchHelper;
56     private RecyclerView recyclerView;
57     private int checkHoldCounter = 0;
58     NewTransactionItemsAdapter(NewTransactionModel viewModel, MobileLedgerProfile profile) {
59         super();
60         model = viewModel;
61         mProfile = profile;
62         int size = model.getAccountCount();
63         while (size < 2) {
64             Logger.debug("new-transaction",
65                     String.format(Locale.US, "%d accounts is too little, Calling addRow()", size));
66             size = addRow();
67         }
68
69         NewTransactionItemsAdapter adapter = this;
70
71         touchHelper = new ItemTouchHelper(new ItemTouchHelper.Callback() {
72             @Override
73             public boolean isLongPressDragEnabled() {
74                 return true;
75             }
76             @Override
77             public boolean canDropOver(@NonNull RecyclerView recyclerView,
78                                        @NonNull RecyclerView.ViewHolder current,
79                                        @NonNull RecyclerView.ViewHolder target) {
80                 final int adapterPosition = target.getAdapterPosition();
81
82                 // first and last items are immovable
83                 if (adapterPosition == 0)
84                     return false;
85                 if (adapterPosition == adapter.getItemCount() - 1)
86                     return false;
87
88                 return super.canDropOver(recyclerView, current, target);
89             }
90             @Override
91             public int getMovementFlags(@NonNull RecyclerView recyclerView,
92                                         @NonNull RecyclerView.ViewHolder viewHolder) {
93                 int flags = makeFlag(ItemTouchHelper.ACTION_STATE_IDLE, ItemTouchHelper.END);
94                 // the top (date and description) and the bottom (padding) items are always there
95                 final int adapterPosition = viewHolder.getAdapterPosition();
96                 if ((adapterPosition > 0) && (adapterPosition < adapter.getItemCount() - 1)) {
97                     flags |= makeFlag(ItemTouchHelper.ACTION_STATE_DRAG,
98                             ItemTouchHelper.UP | ItemTouchHelper.DOWN) |
99                              makeFlag(ItemTouchHelper.ACTION_STATE_SWIPE,
100                                      ItemTouchHelper.START | ItemTouchHelper.END);
101                 }
102
103                 return flags;
104             }
105             @Override
106             public boolean onMove(@NonNull RecyclerView recyclerView,
107                                   @NonNull RecyclerView.ViewHolder viewHolder,
108                                   @NonNull RecyclerView.ViewHolder target) {
109
110                 model.swapItems(viewHolder.getAdapterPosition(), target.getAdapterPosition());
111                 notifyItemMoved(viewHolder.getAdapterPosition(), target.getAdapterPosition());
112                 return true;
113             }
114             @Override
115             public void onSwiped(@NonNull RecyclerView.ViewHolder viewHolder, int direction) {
116                 int pos = viewHolder.getAdapterPosition();
117                 viewModel.removeItem(pos - 1);
118                 notifyItemRemoved(pos);
119                 viewModel.sendCountNotifications(); // needed after items re-arrangement
120                 checkTransactionSubmittable();
121             }
122         });
123     }
124     public void setProfile(MobileLedgerProfile profile) {
125         mProfile = profile;
126     }
127     int addRow() {
128         return addRow(null);
129     }
130     int addRow(String commodity) {
131         final int newAccountCount = model.addAccount(new LedgerTransactionAccount("", commodity));
132         Logger.debug("new-transaction",
133                 String.format(Locale.US, "invoking notifyItemInserted(%d)", newAccountCount));
134         // the header is at position 0
135         notifyItemInserted(newAccountCount);
136         model.sendCountNotifications(); // needed after holders' positions have changed
137         return newAccountCount;
138     }
139     @NonNull
140     @Override
141     public NewTransactionItemHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
142         LinearLayout row = (LinearLayout) LayoutInflater.from(parent.getContext())
143                                                         .inflate(R.layout.new_transaction_row,
144                                                                 parent, false);
145
146         return new NewTransactionItemHolder(row, this);
147     }
148     @Override
149     public void onBindViewHolder(@NonNull NewTransactionItemHolder holder, int position) {
150         Logger.debug("bind", String.format(Locale.US, "Binding item at position %d", position));
151         NewTransactionModel.Item item = model.getItem(position);
152         holder.setData(item);
153         Logger.debug("bind", String.format(Locale.US, "Bound %s item at position %d", item.getType()
154                                                                                           .toString(),
155                 position));
156     }
157     @Override
158     public int getItemCount() {
159         return model.getAccountCount() + 2;
160     }
161     boolean accountListIsEmpty() {
162         for (int i = 0; i < model.getAccountCount(); i++) {
163             LedgerTransactionAccount acc = model.getAccount(i);
164             if (!acc.getAccountName()
165                     .isEmpty())
166                 return false;
167             if (acc.isAmountSet())
168                 return false;
169         }
170
171         return true;
172     }
173     @Override
174     public void onAttachedToRecyclerView(@NonNull RecyclerView recyclerView) {
175         super.onAttachedToRecyclerView(recyclerView);
176         this.recyclerView = recyclerView;
177         touchHelper.attachToRecyclerView(recyclerView);
178     }
179     @Override
180     public void onDetachedFromRecyclerView(@NonNull RecyclerView recyclerView) {
181         touchHelper.attachToRecyclerView(null);
182         super.onDetachedFromRecyclerView(recyclerView);
183         this.recyclerView = null;
184     }
185     public void descriptionSelected(String description) {
186         debug("descr selected", description);
187         if (!accountListIsEmpty())
188             return;
189
190         String accFilter = mProfile.getPreferredAccountsFilter();
191
192         ArrayList<String> params = new ArrayList<>();
193         StringBuilder sb = new StringBuilder(
194                 "select t.profile, t.id from transactions t where t.description=?");
195         params.add(description);
196
197         if (accFilter != null) {
198             sb.append(" AND EXISTS (")
199               .append("SELECT 1 FROM transaction_accounts ta ")
200               .append("WHERE ta.profile = t.profile")
201               .append(" AND ta.transaction_id = t.id")
202               .append(" AND UPPER(ta.account_name) LIKE '%'||?||'%')");
203             params.add(accFilter.toUpperCase());
204         }
205
206         sb.append(" ORDER BY date desc limit 1");
207
208         final String sql = sb.toString();
209         debug("descr", sql);
210         debug("descr", params.toString());
211
212         try (Cursor c = App.getDatabase()
213                            .rawQuery(sql, params.toArray(new String[]{})))
214         {
215             String profileUUID;
216             int transactionId;
217
218             if (!c.moveToNext()) {
219                 sb = new StringBuilder("select t.profile, t.id from transactions t where t.description=?");
220                 sb.append(" ORDER BY date desc LIMIT 1");
221
222                 final String broaderSql = sb.toString();
223                 debug("descr", broaderSql);
224                 debug("descr", params.toString());
225                 try (Cursor c2 = App.getDatabase().rawQuery(broaderSql, new String[]{description})) {
226                     if (!c2.moveToNext()) return;
227
228                     profileUUID = c2.getString(0);
229                     transactionId = c2.getInt(1);
230                 }
231             }
232             else {
233                 profileUUID = c.getString(0);
234                 transactionId = c.getInt(1);
235             }
236
237             loadTransactionIntoModel(profileUUID, transactionId);
238         }
239     }
240     private void loadTransactionIntoModel(String profileUUID, int transactionId) {
241         LedgerTransaction tr;
242         MobileLedgerProfile profile = Data.getProfile(profileUUID);
243         if (profile == null)
244             throw new RuntimeException(String.format(
245                     "Unable to find profile %s, which is supposed to contain transaction %d",
246                     profileUUID, transactionId));
247
248         tr = profile.loadTransaction(transactionId);
249         ArrayList<LedgerTransactionAccount> accounts = tr.getAccounts();
250         NewTransactionModel.Item firstNegative = null;
251         NewTransactionModel.Item firstPositive = null;
252         int singleNegativeIndex = -1;
253         int singlePositiveIndex = -1;
254         int negativeCount = 0;
255         for (int i = 0; i < accounts.size(); i++) {
256             LedgerTransactionAccount acc = accounts.get(i);
257             NewTransactionModel.Item item;
258             if (model.getAccountCount() < i + 1) {
259                 model.addAccount(acc);
260                 notifyItemInserted(i + 1);
261             }
262             item = model.getItem(i + 1);
263
264             item.getAccount()
265                 .setAccountName(acc.getAccountName());
266             item.setComment(acc.getComment());
267             if (acc.isAmountSet()) {
268                 item.getAccount()
269                     .setAmount(acc.getAmount());
270                 if (acc.getAmount() < 0) {
271                     if (firstNegative == null) {
272                         firstNegative = item;
273                         singleNegativeIndex = i;
274                     }
275                     else
276                         singleNegativeIndex = -1;
277                 }
278                 else {
279                     if (firstPositive == null) {
280                         firstPositive = item;
281                         singlePositiveIndex = i;
282                     }
283                     else
284                         singlePositiveIndex = -1;
285                 }
286             }
287             else
288                 item.getAccount()
289                     .resetAmount();
290             notifyItemChanged(i + 1);
291         }
292
293         if (singleNegativeIndex != -1) {
294             firstNegative.getAccount()
295                          .resetAmount();
296             model.moveItemLast(singleNegativeIndex);
297         }
298         else if (singlePositiveIndex != -1) {
299             firstPositive.getAccount()
300                          .resetAmount();
301             model.moveItemLast(singlePositiveIndex);
302         }
303
304         checkTransactionSubmittable();
305         model.setFocusedItem(1);
306     }
307     public void toggleAllEditing(boolean editable) {
308         // item 0 is the header
309         for (int i = 0; i <= model.getAccountCount(); i++) {
310             model.getItem(i)
311                  .setEditable(editable);
312             notifyItemChanged(i);
313             // TODO perhaps do only one notification about the whole range (notifyDatasetChanged)?
314         }
315     }
316     public void reset() {
317         int presentItemCount = model.getAccountCount();
318         model.reset();
319         notifyItemChanged(0);       // header changed
320         notifyItemRangeChanged(1, 2);    // the two empty rows
321         if (presentItemCount > 2)
322             notifyItemRangeRemoved(3, presentItemCount - 2); // all the rest are gone
323     }
324     public void updateFocusedItem(int position) {
325         model.updateFocusedItem(position);
326     }
327     public void noteFocusIsOnAccount(int position) {
328         model.noteFocusChanged(position, NewTransactionModel.FocusedElement.Account);
329     }
330     public void noteFocusIsOnAmount(int position) {
331         model.noteFocusChanged(position, NewTransactionModel.FocusedElement.Amount);
332     }
333     public void noteFocusIsOnComment(int position) {
334         model.noteFocusChanged(position, NewTransactionModel.FocusedElement.Comment);
335     }
336     private void holdSubmittableChecks() {
337         checkHoldCounter++;
338     }
339     private void releaseSubmittableChecks() {
340         if (checkHoldCounter == 0)
341             throw new RuntimeException("Asymmetrical call to releaseSubmittableChecks");
342         checkHoldCounter--;
343     }
344     void setItemCurrency(NewTransactionModel.Item item, Currency newCurrency) {
345         Currency oldCurrency = item.getCurrency();
346         if (!Currency.equal(newCurrency, oldCurrency)) {
347             holdSubmittableChecks();
348             try {
349                 item.setCurrency(newCurrency);
350 //                for (Item i : items) {
351 //                    if (Currency.equal(i.getCurrency(), oldCurrency))
352 //                        i.setCurrency(newCurrency);
353 //                }
354             }
355             finally {
356                 releaseSubmittableChecks();
357             }
358
359             checkTransactionSubmittable();
360         }
361     }
362     /*
363          A transaction is submittable if:
364          0) has description
365          1) has at least two account names
366          2) each row with amount has account name
367          3) for each commodity:
368          3a) amounts must balance to 0, or
369          3b) there must be exactly one empty amount (with account)
370          4) empty accounts with empty amounts are ignored
371          Side effects:
372          5) a row with an empty account name or empty amount is guaranteed to exist for each
373          commodity
374          6) at least two rows need to be present in the ledger
375
376         */
377     @SuppressLint("DefaultLocale")
378     void checkTransactionSubmittable() {
379         if (checkHoldCounter > 0)
380             return;
381
382         int accounts = 0;
383         final BalanceForCurrency balance = new BalanceForCurrency();
384         final String descriptionText = model.getDescription();
385         boolean submittable = true;
386         final ItemsForCurrency itemsForCurrency = new ItemsForCurrency();
387         final ItemsForCurrency itemsWithEmptyAmountForCurrency = new ItemsForCurrency();
388         final ItemsForCurrency itemsWithAccountAndEmptyAmountForCurrency = new ItemsForCurrency();
389         final ItemsForCurrency itemsWithEmptyAccountForCurrency = new ItemsForCurrency();
390         final ItemsForCurrency itemsWithAmountForCurrency = new ItemsForCurrency();
391         final ItemsForCurrency itemsWithAccountForCurrency = new ItemsForCurrency();
392         final ItemsForCurrency emptyRowsForCurrency = new ItemsForCurrency();
393         final List<NewTransactionModel.Item> emptyRows = new ArrayList<>();
394
395         try {
396             if ((descriptionText == null) || descriptionText.trim()
397                                                             .isEmpty())
398             {
399                 Logger.debug("submittable", "Transaction not submittable: missing description");
400                 submittable = false;
401             }
402
403             for (int i = 0; i < model.items.size(); i++) {
404                 NewTransactionModel.Item item = model.items.get(i);
405
406                 LedgerTransactionAccount acc = item.getAccount();
407                 String acc_name = acc.getAccountName()
408                                      .trim();
409                 String currName = acc.getCurrency();
410
411                 itemsForCurrency.add(currName, item);
412
413                 if (acc_name.isEmpty()) {
414                     itemsWithEmptyAccountForCurrency.add(currName, item);
415
416                     if (acc.isAmountSet()) {
417                         // 2) each amount has account name
418                         Logger.debug("submittable", String.format(
419                                 "Transaction not submittable: row %d has no account name, but" +
420                                 " has" + " amount %1.2f", i + 1, acc.getAmount()));
421                         submittable = false;
422                     }
423                     else {
424                         emptyRowsForCurrency.add(currName, item);
425                     }
426                 }
427                 else {
428                     accounts++;
429                     itemsWithAccountForCurrency.add(currName, item);
430                 }
431
432                 if (acc.isAmountSet()) {
433                     itemsWithAmountForCurrency.add(currName, item);
434                     balance.add(currName, acc.getAmount());
435                 }
436                 else {
437                     itemsWithEmptyAmountForCurrency.add(currName, item);
438
439                     if (!acc_name.isEmpty())
440                         itemsWithAccountAndEmptyAmountForCurrency.add(currName, item);
441                 }
442             }
443
444             // 1) has at least two account names
445             if (accounts < 2) {
446                 if (accounts == 0)
447                     Logger.debug("submittable",
448                             "Transaction not submittable: no account " + "names");
449                 else if (accounts == 1)
450                     Logger.debug("submittable",
451                             "Transaction not submittable: only one account name");
452                 else
453                     Logger.debug("submittable",
454                             String.format("Transaction not submittable: only %d account names",
455                                     accounts));
456                 submittable = false;
457             }
458
459             // 3) for each commodity:
460             // 3a) amount must balance to 0, or
461             // 3b) there must be exactly one empty amount (with account)
462             for (String balCurrency : itemsForCurrency.currencies()) {
463                 float currencyBalance = balance.get(balCurrency);
464                 if (Misc.isZero(currencyBalance)) {
465                     // remove hints from all amount inputs in that currency
466                     for (NewTransactionModel.Item item : model.items) {
467                         if (Currency.equal(item.getCurrency(), balCurrency))
468                             item.setAmountHint(null);
469                     }
470                 }
471                 else {
472                     List<NewTransactionModel.Item> list =
473                             itemsWithAccountAndEmptyAmountForCurrency.getList(balCurrency);
474                     int balanceReceiversCount = list.size();
475                     if (balanceReceiversCount != 1) {
476                         if (BuildConfig.DEBUG) {
477                             if (balanceReceiversCount == 0)
478                                 Logger.debug("submittable", String.format(
479                                         "Transaction not submittable [%s]: non-zero balance " +
480                                         "with no empty amounts with accounts", balCurrency));
481                             else
482                                 Logger.debug("submittable", String.format(
483                                         "Transaction not submittable [%s]: non-zero balance " +
484                                         "with multiple empty amounts with accounts", balCurrency));
485                         }
486                         submittable = false;
487                     }
488
489                     List<NewTransactionModel.Item> emptyAmountList =
490                             itemsWithEmptyAmountForCurrency.getList(balCurrency);
491
492                     // suggest off-balance amount to a row and remove hints on other rows
493                     NewTransactionModel.Item receiver = null;
494                     if (!list.isEmpty())
495                         receiver = list.get(0);
496                     else if (!emptyAmountList.isEmpty())
497                         receiver = emptyAmountList.get(0);
498
499                     for (NewTransactionModel.Item item : model.items) {
500                         if (!Currency.equal(item.getCurrency(), balCurrency))
501                             continue;
502
503                         if (item.equals(receiver)) {
504                             if (BuildConfig.DEBUG)
505                                 Logger.debug("submittable",
506                                         String.format("Setting amount hint to %1.2f [%s]",
507                                                 -currencyBalance, balCurrency));
508                             item.setAmountHint(String.format("%1.2f", -currencyBalance));
509                         }
510                         else {
511                             if (BuildConfig.DEBUG)
512                                 Logger.debug("submittable",
513                                         String.format("Resetting hint of '%s' [%s]",
514                                                 (item.getAccount() == null) ? "" : item.getAccount()
515                                                                                        .getAccountName(),
516                                                 balCurrency));
517                             item.setAmountHint(null);
518                         }
519                     }
520                 }
521             }
522
523             // 5) a row with an empty account name or empty amount is guaranteed to exist for
524             // each commodity
525             for (String balCurrency : balance.currencies()) {
526                 int currEmptyRows = itemsWithEmptyAccountForCurrency.size(balCurrency);
527                 int currRows = itemsForCurrency.size(balCurrency);
528                 int currAccounts = itemsWithAccountForCurrency.size(balCurrency);
529                 int currAmounts = itemsWithAmountForCurrency.size(balCurrency);
530                 if ((currEmptyRows == 0) &&
531                     ((currRows == currAccounts) || (currRows == currAmounts)))
532                 {
533                     // perhaps there already is an unused empty row for another currency that
534                     // is not used?
535 //                        boolean foundIt = false;
536 //                        for (Item item : emptyRows) {
537 //                            Currency itemCurrency = item.getCurrency();
538 //                            String itemCurrencyName =
539 //                                    (itemCurrency == null) ? "" : itemCurrency.getName();
540 //                            if (Misc.isZero(balance.get(itemCurrencyName))) {
541 //                                item.setCurrency(Currency.loadByName(balCurrency));
542 //                                item.setAmountHint(
543 //                                        String.format("%1.2f", -balance.get(balCurrency)));
544 //                                foundIt = true;
545 //                                break;
546 //                            }
547 //                        }
548 //
549 //                        if (!foundIt)
550                     addRow(balCurrency);
551                 }
552             }
553
554             // drop extra empty rows, not needed
555             for (String currName : emptyRowsForCurrency.currencies()) {
556                 List<NewTransactionModel.Item> emptyItems = emptyRowsForCurrency.getList(currName);
557                 while ((model.items.size() > 2) && (emptyItems.size() > 1)) {
558                     NewTransactionModel.Item item = emptyItems.get(1);
559                     emptyItems.remove(1);
560                     model.removeRow(item, this);
561                 }
562
563                 // unused currency, remove last item (which is also an empty one)
564                 if ((model.items.size() > 2) && (emptyItems.size() == 1)) {
565                     List<NewTransactionModel.Item> currItems = itemsForCurrency.getList(currName);
566
567                     if (currItems.size() == 1) {
568                         NewTransactionModel.Item item = emptyItems.get(0);
569                         model.removeRow(item, this);
570                     }
571                 }
572             }
573
574             // 6) at least two rows need to be present in the ledger
575             while (model.items.size() < 2)
576                 addRow();
577
578
579             debug("submittable", submittable ? "YES" : "NO");
580             model.isSubmittable.setValue(submittable);
581
582             if (BuildConfig.DEBUG) {
583                 debug("submittable", "== Dump of all items");
584                 for (int i = 0; i < model.items.size(); i++) {
585                     NewTransactionModel.Item item = model.items.get(i);
586                     LedgerTransactionAccount acc = item.getAccount();
587                     debug("submittable", String.format("Item %2d: [%4.2f(%s) %s] %s ; %s", i,
588                             acc.isAmountSet() ? acc.getAmount() : 0,
589                             item.isAmountHintSet() ? item.getAmountHint() : "ø", acc.getCurrency(),
590                             acc.getAccountName(), acc.getComment()));
591                 }
592             }
593         }
594         catch (NumberFormatException e) {
595             debug("submittable", "NO (because of NumberFormatException)");
596             model.isSubmittable.setValue(false);
597         }
598         catch (Exception e) {
599             e.printStackTrace();
600             debug("submittable", "NO (because of an Exception)");
601             model.isSubmittable.setValue(false);
602         }
603     }
604     private class BalanceForCurrency {
605         private HashMap<String, Float> hashMap = new HashMap<>();
606         float get(String currencyName) {
607             Float f = hashMap.get(currencyName);
608             if (f == null) {
609                 f = 0f;
610                 hashMap.put(currencyName, f);
611             }
612             return f;
613         }
614         void add(String currencyName, float amount) {
615             hashMap.put(currencyName, get(currencyName) + amount);
616         }
617         Set<String> currencies() {
618             return hashMap.keySet();
619         }
620         boolean containsCurrency(String currencyName) {
621             return hashMap.containsKey(currencyName);
622         }
623     }
624
625     private class ItemsForCurrency {
626         private HashMap<String, List<NewTransactionModel.Item>> hashMap = new HashMap<>();
627         @NonNull
628         List<NewTransactionModel.Item> getList(@Nullable String currencyName) {
629             List<NewTransactionModel.Item> list = hashMap.get(currencyName);
630             if (list == null) {
631                 list = new ArrayList<>();
632                 hashMap.put(currencyName, list);
633             }
634             return list;
635         }
636         void add(@Nullable String currencyName, @NonNull NewTransactionModel.Item item) {
637             getList(currencyName).add(item);
638         }
639         int size(@Nullable String currencyName) {
640             return this.getList(currencyName)
641                        .size();
642         }
643         Set<String> currencies() {
644             return hashMap.keySet();
645         }
646     }
647 }