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.new_transaction;
20 import android.content.Intent;
21 import android.database.AbstractCursor;
22 import android.database.Cursor;
23 import android.os.Bundle;
24 import android.os.ParcelFormatException;
25 import android.text.TextUtils;
26 import android.util.TypedValue;
27 import android.view.Menu;
28 import android.view.MenuItem;
29 import android.view.View;
31 import androidx.activity.result.ActivityResultLauncher;
32 import androidx.annotation.NonNull;
33 import androidx.lifecycle.LiveData;
34 import androidx.lifecycle.ViewModelProvider;
35 import androidx.navigation.NavController;
36 import androidx.navigation.fragment.NavHostFragment;
38 import com.google.android.material.dialog.MaterialAlertDialogBuilder;
39 import com.google.android.material.snackbar.Snackbar;
41 import net.ktnx.mobileledger.BuildConfig;
42 import net.ktnx.mobileledger.R;
43 import net.ktnx.mobileledger.async.AsyncCrasher;
44 import net.ktnx.mobileledger.async.DescriptionSelectedCallback;
45 import net.ktnx.mobileledger.async.SendTransactionTask;
46 import net.ktnx.mobileledger.async.TaskCallback;
47 import net.ktnx.mobileledger.databinding.ActivityNewTransactionBinding;
48 import net.ktnx.mobileledger.db.DB;
49 import net.ktnx.mobileledger.db.TemplateHeader;
50 import net.ktnx.mobileledger.model.Data;
51 import net.ktnx.mobileledger.model.LedgerTransaction;
52 import net.ktnx.mobileledger.model.MatchedTemplate;
53 import net.ktnx.mobileledger.ui.QR;
54 import net.ktnx.mobileledger.ui.activity.ProfileThemedActivity;
55 import net.ktnx.mobileledger.ui.templates.TemplatesActivity;
56 import net.ktnx.mobileledger.utils.Logger;
57 import net.ktnx.mobileledger.utils.MLDB;
58 import net.ktnx.mobileledger.utils.Misc;
60 import java.util.ArrayList;
61 import java.util.List;
62 import java.util.Objects;
63 import java.util.regex.Matcher;
64 import java.util.regex.Pattern;
66 import static net.ktnx.mobileledger.utils.Logger.debug;
68 public class NewTransactionActivity extends ProfileThemedActivity
69 implements TaskCallback, NewTransactionFragment.OnNewTransactionFragmentInteractionListener,
70 QR.QRScanTrigger, QR.QRScanResultReceiver, DescriptionSelectedCallback {
71 private NavController navController;
72 private NewTransactionModel model;
73 private ActivityResultLauncher<Void> qrScanLauncher;
74 private ActivityNewTransactionBinding b;
76 protected void onCreate(Bundle savedInstanceState) {
77 super.onCreate(savedInstanceState);
79 b = ActivityNewTransactionBinding.inflate(getLayoutInflater(), null, false);
80 setContentView(b.getRoot());
81 setSupportActionBar(b.toolbar);
82 Data.observeProfile(this,
83 mobileLedgerProfile -> b.toolbar.setSubtitle(mobileLedgerProfile.getName()));
85 NavHostFragment navHostFragment = (NavHostFragment) Objects.requireNonNull(
86 getSupportFragmentManager().findFragmentById(R.id.new_transaction_nav));
87 navController = navHostFragment.getNavController();
89 Objects.requireNonNull(getSupportActionBar())
90 .setDisplayHomeAsUpEnabled(true);
92 model = new ViewModelProvider(this).get(NewTransactionModel.class);
94 qrScanLauncher = QR.registerLauncher(this, this);
97 .observe(this, isSubmittable -> {
105 // viewModel.checkTransactionSubmittable(listAdapter);
107 b.fabAdd.setOnClickListener(v -> onFabPressed());
112 protected void initProfile() {
113 String profileUUID = getIntent().getStringExtra("profile_uuid");
115 if (profileUUID != null) {
116 mProfile = Data.getProfile(profileUUID);
117 if (mProfile == null)
119 Data.setCurrentProfile(mProfile);
125 public void finish() {
127 overridePendingTransition(R.anim.dummy, R.anim.slide_out_down);
130 public boolean onOptionsItemSelected(MenuItem item) {
131 if (item.getItemId() == android.R.id.home) {
135 return super.onOptionsItemSelected(item);
137 public void onTransactionSave(LedgerTransaction tr) {
138 navController.navigate(R.id.action_newTransactionFragment_to_newTransactionSavingFragment);
141 SendTransactionTask saver =
142 new SendTransactionTask(this, mProfile, model.getSimulateSaveFlag());
145 catch (Exception e) {
146 debug("new-transaction", "Unknown error", e);
148 Bundle b = new Bundle();
149 b.putString("error", "unknown error");
150 navController.navigate(R.id.newTransactionFragment, b);
153 public void simulateCrash(MenuItem item) {
154 debug("crash", "Will crash intentionally");
155 new AsyncCrasher().execute();
157 public boolean onCreateOptionsMenu(Menu menu) {
158 // Inflate the menu; this adds items to the action bar if it is present.
159 getMenuInflater().inflate(R.menu.new_transaction, menu);
161 if (BuildConfig.DEBUG) {
162 menu.findItem(R.id.action_simulate_crash)
164 menu.findItem(R.id.action_simulate_save)
168 model.getSimulateSave()
169 .observe(this, state -> {
170 menu.findItem(R.id.action_simulate_save)
172 b.simulationLabel.setVisibility(state ? View.VISIBLE : View.GONE);
179 public int dp2px(float dp) {
180 return Math.round(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp,
181 getResources().getDisplayMetrics()));
184 public void done(String error) {
185 Bundle b = new Bundle();
187 b.putString("error", error);
188 navController.navigate(R.id.action_newTransactionSavingFragment_Failure, b);
191 navController.navigate(R.id.action_newTransactionSavingFragment_Success, b);
193 public void toggleSimulateSave(MenuItem item) {
194 model.toggleSimulateSave();
198 public void triggerQRScan() {
199 qrScanLauncher.launch(null);
201 private void startNewPatternActivity(String scanned) {
202 Intent intent = new Intent(this, TemplatesActivity.class);
203 Bundle args = new Bundle();
204 args.putString(TemplatesActivity.ARG_ADD_TEMPLATE, scanned);
205 startActivity(intent, args);
207 private void alertNoTemplateMatch(String scanned) {
208 MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(this);
209 builder.setCancelable(true)
210 .setMessage(R.string.no_template_matches)
211 .setPositiveButton(R.string.add_button,
212 (dialog, which) -> startNewPatternActivity(scanned))
216 public void onQRScanResult(String text) {
217 Logger.debug("qr", String.format("Got QR scan result [%s]", text));
219 if (Misc.emptyIsNull(text) == null)
222 LiveData<List<TemplateHeader>> allTemplates = DB.get()
225 allTemplates.observe(this, templateHeaders -> {
226 ArrayList<MatchedTemplate> matchingFallbackTemplates = new ArrayList<>();
227 ArrayList<MatchedTemplate> matchingTemplates = new ArrayList<>();
229 for (TemplateHeader ph : templateHeaders) {
230 String patternSource = ph.getRegularExpression();
231 if (Misc.emptyIsNull(patternSource) == null)
234 Pattern pattern = Pattern.compile(patternSource);
235 Matcher matcher = pattern.matcher(text);
236 if (!matcher.matches())
239 Logger.debug("pattern",
240 String.format("Pattern '%s' [%s] matches '%s'", ph.getName(),
241 patternSource, text));
243 matchingFallbackTemplates.add(
244 new MatchedTemplate(ph, matcher.toMatchResult()));
246 matchingTemplates.add(new MatchedTemplate(ph, matcher.toMatchResult()));
248 catch (ParcelFormatException e) {
250 Logger.debug("pattern",
251 String.format("Error compiling regular expression '%s'", patternSource),
256 if (matchingTemplates.isEmpty())
257 matchingTemplates = matchingFallbackTemplates;
259 if (matchingTemplates.isEmpty())
260 alertNoTemplateMatch(text);
261 else if (matchingTemplates.size() == 1)
262 model.applyTemplate(matchingTemplates.get(0), text);
264 chooseTemplate(matchingTemplates, text);
267 private void chooseTemplate(ArrayList<MatchedTemplate> matchingTemplates, String matchedText) {
268 final String templateNameColumn = "name";
269 AbstractCursor cursor = new AbstractCursor() {
271 public int getCount() {
272 return matchingTemplates.size();
275 public String[] getColumnNames() {
276 return new String[]{"_id", templateNameColumn};
279 public String getString(int column) {
281 return String.valueOf(getPosition());
282 return matchingTemplates.get(getPosition()).templateHead.getName();
285 public short getShort(int column) {
287 return (short) getPosition();
291 public int getInt(int column) {
292 return getShort(column);
295 public long getLong(int column) {
296 return getShort(column);
299 public float getFloat(int column) {
300 return getShort(column);
303 public double getDouble(int column) {
304 return getShort(column);
307 public boolean isNull(int column) {
311 public int getColumnCount() {
316 MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(this);
317 builder.setCancelable(true)
318 .setTitle(R.string.choose_template_to_apply)
319 .setIcon(R.drawable.ic_baseline_auto_graph_24)
320 .setSingleChoiceItems(cursor, -1, templateNameColumn, (dialog, which) -> {
321 model.applyTemplate(matchingTemplates.get(which), matchedText);
327 public void descriptionSelected(String description) {
328 debug("description selected", description);
329 if (!model.accountListIsEmpty())
332 String accFilter = mProfile.getPreferredAccountsFilter();
334 ArrayList<String> params = new ArrayList<>();
335 StringBuilder sb = new StringBuilder("select t.profile, t.id from transactions t");
337 if (!TextUtils.isEmpty(accFilter)) {
338 sb.append(" JOIN transaction_accounts ta")
339 .append(" ON ta.profile = t.profile")
340 .append(" AND ta.transaction_id = t.id");
343 sb.append(" WHERE t.description=?");
344 params.add(description);
346 if (!TextUtils.isEmpty(accFilter)) {
347 sb.append(" AND ta.account_name LIKE '%'||?||'%'");
348 params.add(accFilter);
351 sb.append(" ORDER BY t.year desc, t.month desc, t.day desc LIMIT 1");
353 final String sql = sb.toString();
354 debug("description", sql);
355 debug("description", params.toString());
357 // FIXME: handle exceptions?
358 MLDB.queryInBackground(sql, params.toArray(new String[]{}), new MLDB.CallbackHelper() {
360 public void onStart() {
361 model.incrementBusyCounter();
364 public void onDone() {
365 model.decrementBusyCounter();
368 public boolean onRow(@NonNull Cursor cursor) {
369 final String profileUUID = cursor.getString(0);
370 final int transactionId = cursor.getInt(1);
371 runOnUiThread(() -> model.loadTransactionIntoModel(profileUUID, transactionId));
372 return false; // limit 1, by the way
375 public void onNoRows() {
376 if (TextUtils.isEmpty(accFilter))
379 debug("description", "Trying transaction search without preferred account filter");
381 final String broaderSql =
382 "select t.profile, t.id from transactions t where t.description=?" +
383 " ORDER BY year desc, month desc, day desc LIMIT 1";
385 debug("description", broaderSql);
386 debug("description", description);
388 runOnUiThread(() -> Snackbar.make(b.newTransactionNav,
389 R.string.ignoring_preferred_account, Snackbar.LENGTH_INDEFINITE)
392 MLDB.queryInBackground(broaderSql, new String[]{description},
393 new MLDB.CallbackHelper() {
395 public void onStart() {
396 model.incrementBusyCounter();
399 public boolean onRow(@NonNull Cursor cursor) {
400 final String profileUUID = cursor.getString(0);
401 final int transactionId = cursor.getInt(1);
402 runOnUiThread(() -> model.loadTransactionIntoModel(profileUUID,
407 public void onDone() {
408 model.decrementBusyCounter();
414 private void onFabPressed() {
416 Misc.hideSoftKeyboard(this);
418 LedgerTransaction tr = model.constructLedgerTransaction();
420 onTransactionSave(tr);