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.
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.
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/>.
18 package net.ktnx.mobileledger.ui.patterns;
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;
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;
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;
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;
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() {
55 setHasStableIds(true);
56 differ = new AsyncListDiffer<>(this, new DiffUtil.ItemCallback<PatternDetailsItem>() {
58 public boolean areItemsTheSame(@NonNull PatternDetailsItem oldItem,
59 @NonNull PatternDetailsItem newItem) {
60 if (oldItem.getType() != newItem.getType())
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()
69 @SuppressLint("DiffUtilEquals")
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();
77 return oldHeader.equalContents(newHeader);
80 PatternDetailsItem.AccountRow oldAcc = oldItem.asAccountRowItem();
81 PatternDetailsItem.AccountRow newAcc = newItem.asAccountRowItem();
83 return oldAcc.equalContents(newAcc);
89 public long getItemId(int position) {
92 PatternDetailsItem.AccountRow accRow = differ.getCurrentList()
95 return accRow.getId();
98 public int getItemViewType(int position) {
100 return differ.getCurrentList()
107 public PatternDetailsAdapter.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent,
109 final LayoutInflater inflater = LayoutInflater.from(parent.getContext());
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));
117 throw new IllegalStateException("Unsupported view type " + viewType);
121 public void onBindViewHolder(@NonNull PatternDetailsAdapter.ViewHolder holder, int position) {
122 PatternDetailsItem item = differ.getCurrentList()
127 public int getItemCount() {
128 return differ.getCurrentList()
131 public void setPatternItems(List<PatternBase> items) {
132 ArrayList<PatternDetailsItem> list = new ArrayList<>();
133 for (PatternBase p : items) {
134 PatternDetailsItem item = PatternDetailsItem.fromRoomObject(p);
139 public void setItems(List<PatternDetailsItem> items) {
140 differ.submitList(items);
142 public String getMatchGroupText(int groupNumber) {
143 PatternDetailsItem.Header header = getHeader();
144 Pattern p = header.getCompiledPattern();
145 if (p == null) return null;
147 Matcher m = p.matcher(header.getTestText());
148 if (m.matches() && m.groupCount() >= groupNumber)
149 return m.group(groupNumber);
153 protected PatternDetailsItem.Header getHeader() {
154 return differ.getCurrentList()
159 private enum HeaderDetail {DESCRIPTION, COMMENT, DATE_YEAR, DATE_MONTH, DATE_DAY}
161 private enum AccDetail {ACCOUNT, COMMENT, AMOUNT}
163 public abstract static class ViewHolder extends RecyclerView.ViewHolder {
164 protected int updateInProgress = 0;
165 ViewHolder(@NonNull View itemView) {
168 protected void startUpdate() {
171 protected void finishUpdate() {
172 if (updateInProgress <= 0)
173 throw new IllegalStateException(
174 "Unexpected updateInProgress value " + updateInProgress);
178 abstract void bind(PatternDetailsItem item);
181 public class Header extends ViewHolder {
182 private final PatternDetailsHeaderBinding b;
183 private final TextWatcher patternNameWatcher = new TextWatcher() {
185 public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
187 public void onTextChanged(CharSequence s, int start, int before, int count) {}
189 public void afterTextChanged(Editable s) {
190 Object tag = b.patternDetailsItemHead.getTag();
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));
200 private final TextWatcher patternWatcher = new TextWatcher() {
202 public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
204 public void onTextChanged(CharSequence s, int start, int before, int count) {}
206 public void afterTextChanged(Editable s) {
207 Object tag = b.patternDetailsItemHead.getTag();
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));
217 private final TextWatcher testTextWatcher = new TextWatcher() {
219 public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
221 public void onTextChanged(CharSequence s, int start, int before, int count) {}
223 public void afterTextChanged(Editable s) {
224 Object tag = b.patternDetailsItemHead.getTag();
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));
234 private final TextWatcher transactionDescriptionWatcher = new TextWatcher() {
236 public void beforeTextChanged(CharSequence s, int start, int count, int after) {
239 public void onTextChanged(CharSequence s, int start, int before, int count) {
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));
251 private final TextWatcher transactionCommentWatcher = new TextWatcher() {
253 public void beforeTextChanged(CharSequence s, int start, int count, int after) {
257 public void onTextChanged(CharSequence s, int start, int before, int count) {
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));
269 public Header(@NonNull PatternDetailsHeaderBinding binding) {
270 super(binding.getRoot());
273 Header(@NonNull View itemView) {
275 throw new IllegalStateException("Should not be used");
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) -> {
287 header.switchToLiteralTransactionDescription();
290 header.switchToLiteralTransactionComment();
293 header.switchToLiteralDateYear();
296 header.switchToLiteralDateMonth();
299 header.switchToLiteralDateDay();
302 throw new IllegalStateException("Unexpected detail " + detail);
308 header.setTransactionDescriptionMatchGroup(group);
311 header.setTransactionCommentMatchGroup(group);
314 header.setDateYearMatchGroup(group);
317 header.setDateMonthMatchGroup(group);
320 header.setDateDayMatchGroup(group);
323 throw new IllegalStateException("Unexpected detail " + detail);
327 notifyItemChanged(getAdapterPosition());
329 final AppCompatActivity activity = (AppCompatActivity) v.getContext();
330 sel.show(activity.getSupportFragmentManager(), "pattern-details-source-selector");
333 void bind(PatternDetailsItem item) {
334 PatternDetailsItem.Header header = item.asHeaderItem();
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());
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);
348 b.patternDetailsDateYearLayout.setVisibility(View.GONE);
349 b.patternDetailsYearSource.setText(String.format(Locale.US, "Group %d (%s)",
350 header.getDateYearMatchGroup(), getMatchGroupText(
351 header.getDateYearMatchGroup())));
353 b.patternDetailsYearSourceLabel.setOnClickListener(
354 v -> selectHeaderDetailSource(v, header, HeaderDetail.DATE_YEAR));
355 b.patternDetailsYearSource.setOnClickListener(
356 v -> selectHeaderDetailSource(v, header, HeaderDetail.DATE_YEAR));
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);
364 b.patternDetailsDateMonthLayout.setVisibility(View.GONE);
365 b.patternDetailsMonthSource.setText(String.format(Locale.US, "Group %d (%s)",
366 header.getDateMonthMatchGroup(), getMatchGroupText(
367 header.getDateMonthMatchGroup())));
369 b.patternDetailsMonthSourceLabel.setOnClickListener(
370 v -> selectHeaderDetailSource(v, header, HeaderDetail.DATE_MONTH));
371 b.patternDetailsMonthSource.setOnClickListener(
372 v -> selectHeaderDetailSource(v, header, HeaderDetail.DATE_MONTH));
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);
380 b.patternDetailsDateDayLayout.setVisibility(View.GONE);
381 b.patternDetailsDaySource.setText(String.format(Locale.US, "Group %d (%s)",
382 header.getDateDayMatchGroup(), getMatchGroupText(
383 header.getDateDayMatchGroup())));
385 b.patternDetailsDaySourceLabel.setOnClickListener(
386 v -> selectHeaderDetailSource(v, header, HeaderDetail.DATE_DAY));
387 b.patternDetailsDaySource.setOnClickListener(
388 v -> selectHeaderDetailSource(v, header, HeaderDetail.DATE_DAY));
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);
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())));
404 b.patternTransactionDescriptionSourceLabel.setOnClickListener(
405 v -> selectHeaderDetailSource(v, header, HeaderDetail.DESCRIPTION));
406 b.patternTransactionDescriptionSource.setOnClickListener(
407 v -> selectHeaderDetailSource(v, header, HeaderDetail.DESCRIPTION));
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);
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())));
423 b.patternTransactionCommentSourceLabel.setOnClickListener(
424 v -> selectHeaderDetailSource(v, header, HeaderDetail.COMMENT));
425 b.patternTransactionCommentSource.setOnClickListener(
426 v -> selectHeaderDetailSource(v, header, HeaderDetail.COMMENT));
428 b.patternDetailsHeadScanQrButton.setOnClickListener(this::scanTestQR);
430 final Object prevTag = b.patternDetailsItemHead.getTag();
431 if (!(prevTag instanceof PatternDetailsItem)) {
432 Logger.debug(D_PATTERN_UI, "Hooked text change listeners");
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);
441 b.patternDetailsItemHead.setTag(item);
447 private void scanTestQR(View view) {
448 QRScanAbleFragment.triggerQRScan();
452 public class AccountRow extends ViewHolder {
453 private final PatternDetailsAccountBinding b;
454 public AccountRow(@NonNull PatternDetailsAccountBinding binding) {
455 super(binding.getRoot());
458 AccountRow(@NonNull View itemView) {
460 throw new IllegalStateException("Should not be used");
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);
471 b.patternDetailsAccountNameLayout.setVisibility(View.GONE);
472 b.patternDetailsAccountNameSource.setText(
473 String.format(Locale.US, "Group %d (%s)", accRow.getAccountNameMatchGroup(),
474 getMatchGroupText(accRow.getAccountNameMatchGroup())));
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);
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())));
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()));
498 b.patternDetailsAccountAmountSource.setText(
499 String.format(Locale.US, "Group %d (%s)", accRow.getAmountMatchGroup(),
500 getMatchGroupText(accRow.getAmountMatchGroup())));
501 b.patternDetailsAccountAmountLayout.setVisibility(View.GONE);
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));
517 private void selectAccountRowDetailSource(View v, PatternDetailsItem.AccountRow accRow,
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) -> {
528 accRow.switchToLiteralAccountName();
531 accRow.switchToLiteralAccountComment();
534 accRow.switchToLiteralAmount();
537 throw new IllegalStateException("Unexpected detail " + detail);
543 accRow.setAccountNameMatchGroup(group);
546 accRow.setAccountCommentMatchGroup(group);
549 accRow.setAmountMatchGroup(group);
552 throw new IllegalStateException("Unexpected detail " + detail);
556 notifyItemChanged(getAdapterPosition());
558 final AppCompatActivity activity = (AppCompatActivity) v.getContext();
559 sel.show(activity.getSupportFragmentManager(), "pattern-details-source-selector");