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