]> git.ktnx.net Git - mobile-ledger.git/blob - app/src/main/java/net/ktnx/mobileledger/ui/activity/NewTransactionItemsAdapter.java
NT: explicitly set the comment when loading a previous transaction
[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     public void toggleComment(int position) {
337         model.toggleComment(position);
338     }
339     private void holdSubmittableChecks() {
340         checkHoldCounter++;
341     }
342     private void releaseSubmittableChecks() {
343         if (checkHoldCounter == 0)
344             throw new RuntimeException("Asymmetrical call to releaseSubmittableChecks");
345         checkHoldCounter--;
346     }
347     void setItemCurrency(NewTransactionModel.Item item, Currency newCurrency) {
348         Currency oldCurrency = item.getCurrency();
349         if (!Currency.equal(newCurrency, oldCurrency)) {
350             holdSubmittableChecks();
351             try {
352                 item.setCurrency(newCurrency);
353 //                for (Item i : items) {
354 //                    if (Currency.equal(i.getCurrency(), oldCurrency))
355 //                        i.setCurrency(newCurrency);
356 //                }
357             }
358             finally {
359                 releaseSubmittableChecks();
360             }
361
362             checkTransactionSubmittable();
363         }
364     }
365     /*
366          A transaction is submittable if:
367          0) has description
368          1) has at least two account names
369          2) each row with amount has account name
370          3) for each commodity:
371          3a) amounts must balance to 0, or
372          3b) there must be exactly one empty amount (with account)
373          4) empty accounts with empty amounts are ignored
374          Side effects:
375          5) a row with an empty account name or empty amount is guaranteed to exist for each
376          commodity
377          6) at least two rows need to be present in the ledger
378
379         */
380     @SuppressLint("DefaultLocale")
381     void checkTransactionSubmittable() {
382         if (checkHoldCounter > 0)
383             return;
384
385         int accounts = 0;
386         final BalanceForCurrency balance = new BalanceForCurrency();
387         final String descriptionText = model.getDescription();
388         boolean submittable = true;
389         final ItemsForCurrency itemsForCurrency = new ItemsForCurrency();
390         final ItemsForCurrency itemsWithEmptyAmountForCurrency = new ItemsForCurrency();
391         final ItemsForCurrency itemsWithAccountAndEmptyAmountForCurrency = new ItemsForCurrency();
392         final ItemsForCurrency itemsWithEmptyAccountForCurrency = new ItemsForCurrency();
393         final ItemsForCurrency itemsWithAmountForCurrency = new ItemsForCurrency();
394         final ItemsForCurrency itemsWithAccountForCurrency = new ItemsForCurrency();
395         final ItemsForCurrency emptyRowsForCurrency = new ItemsForCurrency();
396         final List<NewTransactionModel.Item> emptyRows = new ArrayList<>();
397
398         try {
399             if ((descriptionText == null) || descriptionText.trim()
400                                                             .isEmpty())
401             {
402                 Logger.debug("submittable", "Transaction not submittable: missing description");
403                 submittable = false;
404             }
405
406             for (int i = 0; i < model.items.size(); i++) {
407                 NewTransactionModel.Item item = model.items.get(i);
408
409                 LedgerTransactionAccount acc = item.getAccount();
410                 String acc_name = acc.getAccountName()
411                                      .trim();
412                 String currName = acc.getCurrency();
413
414                 itemsForCurrency.add(currName, item);
415
416                 if (acc_name.isEmpty()) {
417                     itemsWithEmptyAccountForCurrency.add(currName, item);
418
419                     if (acc.isAmountSet()) {
420                         // 2) each amount has account name
421                         Logger.debug("submittable", String.format(
422                                 "Transaction not submittable: row %d has no account name, but" +
423                                 " has" + " amount %1.2f", i + 1, acc.getAmount()));
424                         submittable = false;
425                     }
426                     else {
427                         emptyRowsForCurrency.add(currName, item);
428                     }
429                 }
430                 else {
431                     accounts++;
432                     itemsWithAccountForCurrency.add(currName, item);
433                 }
434
435                 if (acc.isAmountSet()) {
436                     itemsWithAmountForCurrency.add(currName, item);
437                     balance.add(currName, acc.getAmount());
438                 }
439                 else {
440                     itemsWithEmptyAmountForCurrency.add(currName, item);
441
442                     if (!acc_name.isEmpty())
443                         itemsWithAccountAndEmptyAmountForCurrency.add(currName, item);
444                 }
445             }
446
447             // 1) has at least two account names
448             if (accounts < 2) {
449                 if (accounts == 0)
450                     Logger.debug("submittable",
451                             "Transaction not submittable: no account " + "names");
452                 else if (accounts == 1)
453                     Logger.debug("submittable",
454                             "Transaction not submittable: only one account name");
455                 else
456                     Logger.debug("submittable",
457                             String.format("Transaction not submittable: only %d account names",
458                                     accounts));
459                 submittable = false;
460             }
461
462             // 3) for each commodity:
463             // 3a) amount must balance to 0, or
464             // 3b) there must be exactly one empty amount (with account)
465             for (String balCurrency : itemsForCurrency.currencies()) {
466                 float currencyBalance = balance.get(balCurrency);
467                 if (Misc.isZero(currencyBalance)) {
468                     // remove hints from all amount inputs in that currency
469                     for (NewTransactionModel.Item item : model.items) {
470                         if (Currency.equal(item.getCurrency(), balCurrency))
471                             item.setAmountHint(null);
472                     }
473                 }
474                 else {
475                     List<NewTransactionModel.Item> list =
476                             itemsWithAccountAndEmptyAmountForCurrency.getList(balCurrency);
477                     int balanceReceiversCount = list.size();
478                     if (balanceReceiversCount != 1) {
479                         if (BuildConfig.DEBUG) {
480                             if (balanceReceiversCount == 0)
481                                 Logger.debug("submittable", String.format(
482                                         "Transaction not submittable [%s]: non-zero balance " +
483                                         "with no empty amounts with accounts", balCurrency));
484                             else
485                                 Logger.debug("submittable", String.format(
486                                         "Transaction not submittable [%s]: non-zero balance " +
487                                         "with multiple empty amounts with accounts", balCurrency));
488                         }
489                         submittable = false;
490                     }
491
492                     List<NewTransactionModel.Item> emptyAmountList =
493                             itemsWithEmptyAmountForCurrency.getList(balCurrency);
494
495                     // suggest off-balance amount to a row and remove hints on other rows
496                     NewTransactionModel.Item receiver = null;
497                     if (!list.isEmpty())
498                         receiver = list.get(0);
499                     else if (!emptyAmountList.isEmpty())
500                         receiver = emptyAmountList.get(0);
501
502                     for (NewTransactionModel.Item item : model.items) {
503                         if (!Currency.equal(item.getCurrency(), balCurrency))
504                             continue;
505
506                         if (item.equals(receiver)) {
507                             if (BuildConfig.DEBUG)
508                                 Logger.debug("submittable",
509                                         String.format("Setting amount hint to %1.2f [%s]",
510                                                 -currencyBalance, balCurrency));
511                             item.setAmountHint(String.format("%1.2f", -currencyBalance));
512                         }
513                         else {
514                             if (BuildConfig.DEBUG)
515                                 Logger.debug("submittable",
516                                         String.format("Resetting hint of '%s' [%s]",
517                                                 (item.getAccount() == null) ? "" : item.getAccount()
518                                                                                        .getAccountName(),
519                                                 balCurrency));
520                             item.setAmountHint(null);
521                         }
522                     }
523                 }
524             }
525
526             // 5) a row with an empty account name or empty amount is guaranteed to exist for
527             // each commodity
528             for (String balCurrency : balance.currencies()) {
529                 int currEmptyRows = itemsWithEmptyAccountForCurrency.size(balCurrency);
530                 int currRows = itemsForCurrency.size(balCurrency);
531                 int currAccounts = itemsWithAccountForCurrency.size(balCurrency);
532                 int currAmounts = itemsWithAmountForCurrency.size(balCurrency);
533                 if ((currEmptyRows == 0) &&
534                     ((currRows == currAccounts) || (currRows == currAmounts)))
535                 {
536                     // perhaps there already is an unused empty row for another currency that
537                     // is not used?
538 //                        boolean foundIt = false;
539 //                        for (Item item : emptyRows) {
540 //                            Currency itemCurrency = item.getCurrency();
541 //                            String itemCurrencyName =
542 //                                    (itemCurrency == null) ? "" : itemCurrency.getName();
543 //                            if (Misc.isZero(balance.get(itemCurrencyName))) {
544 //                                item.setCurrency(Currency.loadByName(balCurrency));
545 //                                item.setAmountHint(
546 //                                        String.format("%1.2f", -balance.get(balCurrency)));
547 //                                foundIt = true;
548 //                                break;
549 //                            }
550 //                        }
551 //
552 //                        if (!foundIt)
553                     addRow(balCurrency);
554                 }
555             }
556
557             // drop extra empty rows, not needed
558             for (String currName : emptyRowsForCurrency.currencies()) {
559                 List<NewTransactionModel.Item> emptyItems = emptyRowsForCurrency.getList(currName);
560                 while ((model.items.size() > 2) && (emptyItems.size() > 1)) {
561                     NewTransactionModel.Item item = emptyItems.get(1);
562                     emptyItems.remove(1);
563                     model.removeRow(item, this);
564                 }
565
566                 // unused currency, remove last item (which is also an empty one)
567                 if ((model.items.size() > 2) && (emptyItems.size() == 1)) {
568                     List<NewTransactionModel.Item> currItems = itemsForCurrency.getList(currName);
569
570                     if (currItems.size() == 1) {
571                         NewTransactionModel.Item item = emptyItems.get(0);
572                         model.removeRow(item, this);
573                     }
574                 }
575             }
576
577             // 6) at least two rows need to be present in the ledger
578             while (model.items.size() < 2)
579                 addRow();
580
581
582             debug("submittable", submittable ? "YES" : "NO");
583             model.isSubmittable.setValue(submittable);
584
585             if (BuildConfig.DEBUG) {
586                 debug("submittable", "== Dump of all items");
587                 for (int i = 0; i < model.items.size(); i++) {
588                     NewTransactionModel.Item item = model.items.get(i);
589                     LedgerTransactionAccount acc = item.getAccount();
590                     debug("submittable", String.format("Item %2d: [%4.2f(%s) %s] %s ; %s", i,
591                             acc.isAmountSet() ? acc.getAmount() : 0,
592                             item.isAmountHintSet() ? item.getAmountHint() : "ø", acc.getCurrency(),
593                             acc.getAccountName(), acc.getComment()));
594                 }
595             }
596         }
597         catch (NumberFormatException e) {
598             debug("submittable", "NO (because of NumberFormatException)");
599             model.isSubmittable.setValue(false);
600         }
601         catch (Exception e) {
602             e.printStackTrace();
603             debug("submittable", "NO (because of an Exception)");
604             model.isSubmittable.setValue(false);
605         }
606     }
607     private class BalanceForCurrency {
608         private HashMap<String, Float> hashMap = new HashMap<>();
609         float get(String currencyName) {
610             Float f = hashMap.get(currencyName);
611             if (f == null) {
612                 f = 0f;
613                 hashMap.put(currencyName, f);
614             }
615             return f;
616         }
617         void add(String currencyName, float amount) {
618             hashMap.put(currencyName, get(currencyName) + amount);
619         }
620         Set<String> currencies() {
621             return hashMap.keySet();
622         }
623         boolean containsCurrency(String currencyName) {
624             return hashMap.containsKey(currencyName);
625         }
626     }
627
628     private class ItemsForCurrency {
629         private HashMap<String, List<NewTransactionModel.Item>> hashMap = new HashMap<>();
630         @NonNull
631         List<NewTransactionModel.Item> getList(@Nullable String currencyName) {
632             List<NewTransactionModel.Item> list = hashMap.get(currencyName);
633             if (list == null) {
634                 list = new ArrayList<>();
635                 hashMap.put(currencyName, list);
636             }
637             return list;
638         }
639         void add(@Nullable String currencyName, @NonNull NewTransactionModel.Item item) {
640             getList(currencyName).add(item);
641         }
642         int size(@Nullable String currencyName) {
643             return this.getList(currencyName)
644                        .size();
645         }
646         Set<String> currencies() {
647             return hashMap.keySet();
648         }
649     }
650 }