]> git.ktnx.net Git - mobile-ledger.git/blob - app/src/main/java/net/ktnx/mobileledger/ui/patterns/PatternDetailsAdapter.java
5b6ab814c4930bd8103dfa276ce89a86b2cd0901
[mobile-ledger.git] / app / src / main / java / net / ktnx / mobileledger / ui / patterns / PatternDetailsAdapter.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.patterns;
19
20 import android.annotation.SuppressLint;
21 import android.text.Editable;
22 import android.text.TextWatcher;
23 import android.view.LayoutInflater;
24 import android.view.View;
25 import android.view.ViewGroup;
26
27 import androidx.annotation.NonNull;
28 import androidx.appcompat.app.AppCompatActivity;
29 import androidx.recyclerview.widget.AsyncListDiffer;
30 import androidx.recyclerview.widget.DiffUtil;
31 import androidx.recyclerview.widget.RecyclerView;
32
33 import net.ktnx.mobileledger.R;
34 import net.ktnx.mobileledger.databinding.PatternDetailsAccountBinding;
35 import net.ktnx.mobileledger.databinding.PatternDetailsHeaderBinding;
36 import net.ktnx.mobileledger.db.PatternBase;
37 import net.ktnx.mobileledger.model.Data;
38 import net.ktnx.mobileledger.model.PatternDetailsItem;
39 import net.ktnx.mobileledger.ui.PatternDetailSourceSelectorFragment;
40 import net.ktnx.mobileledger.ui.QRScanAbleFragment;
41 import net.ktnx.mobileledger.utils.Logger;
42
43 import java.util.ArrayList;
44 import java.util.List;
45 import java.util.Locale;
46 import java.util.Objects;
47 import java.util.regex.Matcher;
48 import java.util.regex.Pattern;
49
50 class PatternDetailsAdapter extends RecyclerView.Adapter<PatternDetailsAdapter.ViewHolder> {
51     private static final String D_PATTERN_UI = "pattern-ui";
52     private final AsyncListDiffer<PatternDetailsItem> differ;
53     public PatternDetailsAdapter() {
54         super();
55         setHasStableIds(true);
56         differ = new AsyncListDiffer<>(this, new DiffUtil.ItemCallback<PatternDetailsItem>() {
57             @Override
58             public boolean areItemsTheSame(@NonNull PatternDetailsItem oldItem,
59                                            @NonNull PatternDetailsItem newItem) {
60                 if (oldItem.getType() != newItem.getType())
61                     return false;
62                 if (oldItem.getType() == PatternDetailsItem.Type.HEADER)
63                     return true;    // only one header item, ever
64                 // the rest is comparing two account row items
65                 return oldItem.asAccountRowItem()
66                               .getId() == newItem.asAccountRowItem()
67                                                  .getId();
68             }
69             @SuppressLint("DiffUtilEquals")
70             @Override
71             public boolean areContentsTheSame(@NonNull PatternDetailsItem oldItem,
72                                               @NonNull PatternDetailsItem newItem) {
73                 if (oldItem.getType() == PatternDetailsItem.Type.HEADER) {
74                     PatternDetailsItem.Header oldHeader = oldItem.asHeaderItem();
75                     PatternDetailsItem.Header newHeader = newItem.asHeaderItem();
76
77                     return oldHeader.equalContents(newHeader);
78                 }
79                 else {
80                     PatternDetailsItem.AccountRow oldAcc = oldItem.asAccountRowItem();
81                     PatternDetailsItem.AccountRow newAcc = newItem.asAccountRowItem();
82
83                     return oldAcc.equalContents(newAcc);
84                 }
85             }
86         });
87     }
88     @Override
89     public long getItemId(int position) {
90         if (position == 0)
91             return -1;
92         PatternDetailsItem.AccountRow accRow = differ.getCurrentList()
93                                                      .get(position)
94                                                      .asAccountRowItem();
95         return accRow.getId();
96     }
97     @Override
98     public int getItemViewType(int position) {
99
100         return differ.getCurrentList()
101                      .get(position)
102                      .getType()
103                      .toInt();
104     }
105     @NonNull
106     @Override
107     public PatternDetailsAdapter.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent,
108                                                                int viewType) {
109         final LayoutInflater inflater = LayoutInflater.from(parent.getContext());
110         switch (viewType) {
111             case PatternDetailsItem.TYPE.header:
112                 return new Header(PatternDetailsHeaderBinding.inflate(inflater, parent, false));
113             case PatternDetailsItem.TYPE.accountItem:
114                 return new AccountRow(
115                         PatternDetailsAccountBinding.inflate(inflater, parent, false));
116             default:
117                 throw new IllegalStateException("Unsupported view type " + viewType);
118         }
119     }
120     @Override
121     public void onBindViewHolder(@NonNull PatternDetailsAdapter.ViewHolder holder, int position) {
122         PatternDetailsItem item = differ.getCurrentList()
123                                         .get(position);
124         holder.bind(item);
125     }
126     @Override
127     public int getItemCount() {
128         return differ.getCurrentList()
129                      .size();
130     }
131     public void setPatternItems(List<PatternBase> items) {
132         ArrayList<PatternDetailsItem> list = new ArrayList<>();
133         for (PatternBase p : items) {
134             PatternDetailsItem item = PatternDetailsItem.fromRoomObject(p);
135             list.add(item);
136         }
137         setItems(list);
138     }
139     public void setItems(List<PatternDetailsItem> items) {
140         differ.submitList(items);
141     }
142     public String getMatchGroupText(int groupNumber) {
143         PatternDetailsItem.Header header = getHeader();
144         Pattern p = header.getCompiledPattern();
145         if (p == null) return null;
146
147         Matcher m = p.matcher(header.getTestText());
148         if (m.matches() && m.groupCount() >= groupNumber)
149             return m.group(groupNumber);
150         else
151             return null;
152     }
153     protected PatternDetailsItem.Header getHeader() {
154         return differ.getCurrentList()
155                      .get(0)
156                      .asHeaderItem();
157     }
158
159     private enum HeaderDetail {DESCRIPTION, COMMENT, DATE_YEAR, DATE_MONTH, DATE_DAY}
160
161     private enum AccDetail {ACCOUNT, COMMENT, AMOUNT}
162
163     public abstract static class ViewHolder extends RecyclerView.ViewHolder {
164         protected int updateInProgress = 0;
165         ViewHolder(@NonNull View itemView) {
166             super(itemView);
167         }
168         protected void startUpdate() {
169             updateInProgress++;
170         }
171         protected void finishUpdate() {
172             if (updateInProgress <= 0)
173                 throw new IllegalStateException(
174                         "Unexpected updateInProgress value " + updateInProgress);
175
176             updateInProgress--;
177         }
178         abstract void bind(PatternDetailsItem item);
179     }
180
181     public class Header extends ViewHolder {
182         private final PatternDetailsHeaderBinding b;
183         private final TextWatcher patternNameWatcher = new TextWatcher() {
184             @Override
185             public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
186             @Override
187             public void onTextChanged(CharSequence s, int start, int before, int count) {}
188             @Override
189             public void afterTextChanged(Editable s) {
190                 Object tag = b.patternDetailsItemHead.getTag();
191                 if (tag != null) {
192                     final PatternDetailsItem.Header header =
193                             ((PatternDetailsItem) tag).asHeaderItem();
194                     Logger.debug(D_PATTERN_UI,
195                             "Storing changed pattern name " + s + "; header=" + header);
196                     header.setName(String.valueOf(s));
197                 }
198             }
199         };
200         private final TextWatcher patternWatcher = new TextWatcher() {
201             @Override
202             public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
203             @Override
204             public void onTextChanged(CharSequence s, int start, int before, int count) {}
205             @Override
206             public void afterTextChanged(Editable s) {
207                 Object tag = b.patternDetailsItemHead.getTag();
208                 if (tag != null) {
209                     final PatternDetailsItem.Header header =
210                             ((PatternDetailsItem) tag).asHeaderItem();
211                     Logger.debug(D_PATTERN_UI,
212                             "Storing changed pattern " + s + "; header=" + header);
213                     header.setPattern(String.valueOf(s));
214                 }
215             }
216         };
217         private final TextWatcher testTextWatcher = new TextWatcher() {
218             @Override
219             public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
220             @Override
221             public void onTextChanged(CharSequence s, int start, int before, int count) {}
222             @Override
223             public void afterTextChanged(Editable s) {
224                 Object tag = b.patternDetailsItemHead.getTag();
225                 if (tag != null) {
226                     final PatternDetailsItem.Header header =
227                             ((PatternDetailsItem) tag).asHeaderItem();
228                     Logger.debug(D_PATTERN_UI,
229                             "Storing changed test text " + s + "; header=" + header);
230                     header.setTestText(String.valueOf(s));
231                 }
232             }
233         };
234         private final TextWatcher transactionDescriptionWatcher = new TextWatcher() {
235             @Override
236             public void beforeTextChanged(CharSequence s, int start, int count, int after) {
237             }
238             @Override
239             public void onTextChanged(CharSequence s, int start, int before, int count) {
240
241             }
242             @Override
243             public void afterTextChanged(Editable s) {
244                 PatternDetailsItem.Header header = ((PatternDetailsItem) Objects.requireNonNull(
245                         b.patternDetailsItemHead.getTag())).asHeaderItem();
246                 Logger.debug(D_PATTERN_UI,
247                         "Storing changed transaction description " + s + "; header=" + header);
248                 header.setTransactionDescription(String.valueOf(s));
249             }
250         };
251         private final TextWatcher transactionCommentWatcher = new TextWatcher() {
252             @Override
253             public void beforeTextChanged(CharSequence s, int start, int count, int after) {
254
255             }
256             @Override
257             public void onTextChanged(CharSequence s, int start, int before, int count) {
258
259             }
260             @Override
261             public void afterTextChanged(Editable s) {
262                 PatternDetailsItem.Header header = ((PatternDetailsItem) Objects.requireNonNull(
263                         b.patternDetailsItemHead.getTag())).asHeaderItem();
264                 Logger.debug(D_PATTERN_UI,
265                         "Storing changed transaction description " + s + "; header=" + header);
266                 header.setTransactionComment(String.valueOf(s));
267             }
268         };
269         public Header(@NonNull PatternDetailsHeaderBinding binding) {
270             super(binding.getRoot());
271             b = binding;
272         }
273         Header(@NonNull View itemView) {
274             super(itemView);
275             throw new IllegalStateException("Should not be used");
276         }
277         private void selectHeaderDetailSource(View v, PatternDetailsItem.Header header,
278                                               HeaderDetail detail) {
279             Logger.debug(D_PATTERN_UI, "header is " + header);
280             PatternDetailSourceSelectorFragment sel =
281                     PatternDetailSourceSelectorFragment.newInstance(1, header.getPattern(),
282                             header.getTestText());
283             sel.setOnSourceSelectedListener((literal, group) -> {
284                 if (literal) {
285                     switch (detail) {
286                         case DESCRIPTION:
287                             header.switchToLiteralTransactionDescription();
288                             break;
289                         case COMMENT:
290                             header.switchToLiteralTransactionComment();
291                             break;
292                         case DATE_YEAR:
293                             header.switchToLiteralDateYear();
294                             break;
295                         case DATE_MONTH:
296                             header.switchToLiteralDateMonth();
297                             break;
298                         case DATE_DAY:
299                             header.switchToLiteralDateDay();
300                             break;
301                         default:
302                             throw new IllegalStateException("Unexpected detail " + detail);
303                     }
304                 }
305                 else {
306                     switch (detail) {
307                         case DESCRIPTION:
308                             header.setTransactionDescriptionMatchGroup(group);
309                             break;
310                         case COMMENT:
311                             header.setTransactionCommentMatchGroup(group);
312                             break;
313                         case DATE_YEAR:
314                             header.setDateYearMatchGroup(group);
315                             break;
316                         case DATE_MONTH:
317                             header.setDateMonthMatchGroup(group);
318                             break;
319                         case DATE_DAY:
320                             header.setDateDayMatchGroup(group);
321                             break;
322                         default:
323                             throw new IllegalStateException("Unexpected detail " + detail);
324                     }
325                 }
326
327                 notifyItemChanged(getAdapterPosition());
328             });
329             final AppCompatActivity activity = (AppCompatActivity) v.getContext();
330             sel.show(activity.getSupportFragmentManager(), "pattern-details-source-selector");
331         }
332         @Override
333         void bind(PatternDetailsItem item) {
334             PatternDetailsItem.Header header = item.asHeaderItem();
335             startUpdate();
336             try {
337                 Logger.debug(D_PATTERN_UI, "Binding to header " + header);
338                 b.patternName.setText(header.getName());
339                 b.pattern.setText(header.getPattern());
340                 b.testText.setText(header.getTestText());
341
342                 if (header.hasLiteralDateYear()) {
343                     b.patternDetailsYearSource.setText(R.string.pattern_details_source_literal);
344                     b.patternDetailsDateYear.setText(String.valueOf(header.getDateYear()));
345                     b.patternDetailsDateYearLayout.setVisibility(View.VISIBLE);
346                 }
347                 else {
348                     b.patternDetailsDateYearLayout.setVisibility(View.GONE);
349                     b.patternDetailsYearSource.setText(String.format(Locale.US, "Group %d (%s)",
350                             header.getDateYearMatchGroup(), getMatchGroupText(
351                                     header.getDateYearMatchGroup())));
352                 }
353                 b.patternDetailsYearSourceLabel.setOnClickListener(
354                         v -> selectHeaderDetailSource(v, header, HeaderDetail.DATE_YEAR));
355                 b.patternDetailsYearSource.setOnClickListener(
356                         v -> selectHeaderDetailSource(v, header, HeaderDetail.DATE_YEAR));
357
358                 if (header.hasLiteralDateMonth()) {
359                     b.patternDetailsMonthSource.setText(R.string.pattern_details_source_literal);
360                     b.patternDetailsDateMonth.setText(String.valueOf(header.getDateMonth()));
361                     b.patternDetailsDateMonthLayout.setVisibility(View.VISIBLE);
362                 }
363                 else {
364                     b.patternDetailsDateMonthLayout.setVisibility(View.GONE);
365                     b.patternDetailsMonthSource.setText(String.format(Locale.US, "Group %d (%s)",
366                             header.getDateMonthMatchGroup(), getMatchGroupText(
367                                     header.getDateMonthMatchGroup())));
368                 }
369                 b.patternDetailsMonthSourceLabel.setOnClickListener(
370                         v -> selectHeaderDetailSource(v, header, HeaderDetail.DATE_MONTH));
371                 b.patternDetailsMonthSource.setOnClickListener(
372                         v -> selectHeaderDetailSource(v, header, HeaderDetail.DATE_MONTH));
373
374                 if (header.hasLiteralDateDay()) {
375                     b.patternDetailsDaySource.setText(R.string.pattern_details_source_literal);
376                     b.patternDetailsDateDay.setText(String.valueOf(header.getDateDay()));
377                     b.patternDetailsDateDayLayout.setVisibility(View.VISIBLE);
378                 }
379                 else {
380                     b.patternDetailsDateDayLayout.setVisibility(View.GONE);
381                     b.patternDetailsDaySource.setText(String.format(Locale.US, "Group %d (%s)",
382                             header.getDateDayMatchGroup(), getMatchGroupText(
383                                     header.getDateDayMatchGroup())));
384                 }
385                 b.patternDetailsDaySourceLabel.setOnClickListener(
386                         v -> selectHeaderDetailSource(v, header, HeaderDetail.DATE_DAY));
387                 b.patternDetailsDaySource.setOnClickListener(
388                         v -> selectHeaderDetailSource(v, header, HeaderDetail.DATE_DAY));
389
390                 if (header.hasLiteralTransactionDescription()) {
391                     b.patternTransactionDescriptionSource.setText(
392                             R.string.pattern_details_source_literal);
393                     b.transactionDescription.setText(header.getTransactionDescription());
394                     b.transactionDescriptionLayout.setVisibility(View.VISIBLE);
395                 }
396                 else {
397                     b.transactionDescriptionLayout.setVisibility(View.GONE);
398                     b.patternTransactionDescriptionSource.setText(
399                             String.format(Locale.US, "Group %d (%s)",
400                                     header.getTransactionDescriptionMatchGroup(), getMatchGroupText(
401                                             header.getTransactionDescriptionMatchGroup())));
402
403                 }
404                 b.patternTransactionDescriptionSourceLabel.setOnClickListener(
405                         v -> selectHeaderDetailSource(v, header, HeaderDetail.DESCRIPTION));
406                 b.patternTransactionDescriptionSource.setOnClickListener(
407                         v -> selectHeaderDetailSource(v, header, HeaderDetail.DESCRIPTION));
408
409                 if (header.hasLiteralTransactionComment()) {
410                     b.patternTransactionCommentSource.setText(
411                             R.string.pattern_details_source_literal);
412                     b.transactionComment.setText(header.getTransactionComment());
413                     b.transactionCommentLayout.setVisibility(View.VISIBLE);
414                 }
415                 else {
416                     b.transactionCommentLayout.setVisibility(View.GONE);
417                     b.patternTransactionCommentSource.setText(
418                             String.format(Locale.US, "Group %d (%s)",
419                                     header.getTransactionCommentMatchGroup(),
420                                     getMatchGroupText(header.getTransactionCommentMatchGroup())));
421
422                 }
423                 b.patternTransactionCommentSourceLabel.setOnClickListener(
424                         v -> selectHeaderDetailSource(v, header, HeaderDetail.COMMENT));
425                 b.patternTransactionCommentSource.setOnClickListener(
426                         v -> selectHeaderDetailSource(v, header, HeaderDetail.COMMENT));
427
428                 b.patternDetailsHeadScanQrButton.setOnClickListener(this::scanTestQR);
429
430                 final Object prevTag = b.patternDetailsItemHead.getTag();
431                 if (!(prevTag instanceof PatternDetailsItem)) {
432                     Logger.debug(D_PATTERN_UI, "Hooked text change listeners");
433
434                     b.patternName.addTextChangedListener(patternNameWatcher);
435                     b.pattern.addTextChangedListener(patternWatcher);
436                     b.testText.addTextChangedListener(testTextWatcher);
437                     b.transactionDescription.addTextChangedListener(transactionDescriptionWatcher);
438                     b.transactionComment.addTextChangedListener(transactionCommentWatcher);
439                 }
440
441                 b.patternDetailsItemHead.setTag(item);
442             }
443             finally {
444                 finishUpdate();
445             }
446         }
447         private void scanTestQR(View view) {
448             QRScanAbleFragment.triggerQRScan();
449         }
450     }
451
452     public class AccountRow extends ViewHolder {
453         private final PatternDetailsAccountBinding b;
454         public AccountRow(@NonNull PatternDetailsAccountBinding binding) {
455             super(binding.getRoot());
456             b = binding;
457         }
458         AccountRow(@NonNull View itemView) {
459             super(itemView);
460             throw new IllegalStateException("Should not be used");
461         }
462         @Override
463         void bind(PatternDetailsItem item) {
464             PatternDetailsItem.AccountRow accRow = item.asAccountRowItem();
465             if (accRow.hasLiteralAccountName()) {
466                 b.patternDetailsAccountNameLayout.setVisibility(View.VISIBLE);
467                 b.patternDetailsAccountName.setText(accRow.getAccountName());
468                 b.patternDetailsAccountNameSource.setText(R.string.pattern_details_source_literal);
469             }
470             else {
471                 b.patternDetailsAccountNameLayout.setVisibility(View.GONE);
472                 b.patternDetailsAccountNameSource.setText(
473                         String.format(Locale.US, "Group %d (%s)", accRow.getAccountNameMatchGroup(),
474                                 getMatchGroupText(accRow.getAccountNameMatchGroup())));
475             }
476
477             if (accRow.hasLiteralAccountComment()) {
478                 b.patternDetailsAccountCommentLayout.setVisibility(View.VISIBLE);
479                 b.patternDetailsAccountComment.setText(accRow.getAccountComment());
480                 b.patternDetailsAccountCommentSource.setText(
481                         R.string.pattern_details_source_literal);
482             }
483             else {
484                 b.patternDetailsAccountCommentLayout.setVisibility(View.GONE);
485                 b.patternDetailsAccountCommentSource.setText(
486                         String.format(Locale.US, "Group %d (%s)",
487                                 accRow.getAccountCommentMatchGroup(),
488                                 getMatchGroupText(accRow.getAccountCommentMatchGroup())));
489             }
490
491             if (accRow.hasLiteralAmount()) {
492                 b.patternDetailsAccountAmountSource.setText(
493                         R.string.pattern_details_source_literal);
494                 b.patternDetailsAccountAmount.setVisibility(View.VISIBLE);
495                 b.patternDetailsAccountAmount.setText(Data.formatNumber(accRow.getAmount()));
496             }
497             else {
498                 b.patternDetailsAccountAmountSource.setText(
499                         String.format(Locale.US, "Group %d (%s)", accRow.getAmountMatchGroup(),
500                                 getMatchGroupText(accRow.getAmountMatchGroup())));
501                 b.patternDetailsAccountAmountLayout.setVisibility(View.GONE);
502             }
503
504             b.patternAccountNameSourceLabel.setOnClickListener(
505                     v -> selectAccountRowDetailSource(v, accRow, AccDetail.ACCOUNT));
506             b.patternDetailsAccountNameSource.setOnClickListener(
507                     v -> selectAccountRowDetailSource(v, accRow, AccDetail.ACCOUNT));
508             b.patternAccountCommentSourceLabel.setOnClickListener(
509                     v -> selectAccountRowDetailSource(v, accRow, AccDetail.COMMENT));
510             b.patternDetailsAccountCommentSource.setOnClickListener(
511                     v -> selectAccountRowDetailSource(v, accRow, AccDetail.COMMENT));
512             b.patternAccountAmountSourceLabel.setOnClickListener(
513                     v -> selectAccountRowDetailSource(v, accRow, AccDetail.AMOUNT));
514             b.patternDetailsAccountAmountSource.setOnClickListener(
515                     v -> selectAccountRowDetailSource(v, accRow, AccDetail.AMOUNT));
516         }
517         private void selectAccountRowDetailSource(View v, PatternDetailsItem.AccountRow accRow,
518                                                   AccDetail detail) {
519             final PatternDetailsItem.Header header = getHeader();
520             Logger.debug(D_PATTERN_UI, "header is " + header);
521             PatternDetailSourceSelectorFragment sel =
522                     PatternDetailSourceSelectorFragment.newInstance(1, header.getPattern(),
523                             header.getTestText());
524             sel.setOnSourceSelectedListener((literal, group) -> {
525                 if (literal) {
526                     switch (detail) {
527                         case ACCOUNT:
528                             accRow.switchToLiteralAccountName();
529                             break;
530                         case COMMENT:
531                             accRow.switchToLiteralAccountComment();
532                             break;
533                         case AMOUNT:
534                             accRow.switchToLiteralAmount();
535                             break;
536                         default:
537                             throw new IllegalStateException("Unexpected detail " + detail);
538                     }
539                 }
540                 else {
541                     switch (detail) {
542                         case ACCOUNT:
543                             accRow.setAccountNameMatchGroup(group);
544                             break;
545                         case COMMENT:
546                             accRow.setAccountCommentMatchGroup(group);
547                             break;
548                         case AMOUNT:
549                             accRow.setAmountMatchGroup(group);
550                             break;
551                         default:
552                             throw new IllegalStateException("Unexpected detail " + detail);
553                     }
554                 }
555
556                 notifyItemChanged(getAdapterPosition());
557             });
558             final AppCompatActivity activity = (AppCompatActivity) v.getContext();
559             sel.show(activity.getSupportFragmentManager(), "pattern-details-source-selector");
560         }
561     }
562 }