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.Context;
21 import android.content.Intent;
22 import android.database.AbstractCursor;
23 import android.database.Cursor;
24 import android.os.Bundle;
25 import android.os.ParcelFormatException;
26 import android.text.TextUtils;
27 import android.util.TypedValue;
28 import android.view.Menu;
29 import android.view.MenuItem;
30 import android.view.View;
32 import androidx.activity.result.ActivityResultLauncher;
33 import androidx.annotation.NonNull;
34 import androidx.lifecycle.LiveData;
35 import androidx.lifecycle.ViewModelProvider;
36 import androidx.navigation.NavController;
37 import androidx.navigation.fragment.NavHostFragment;
39 import com.google.android.material.dialog.MaterialAlertDialogBuilder;
40 import com.google.android.material.snackbar.Snackbar;
42 import net.ktnx.mobileledger.BuildConfig;
43 import net.ktnx.mobileledger.R;
44 import net.ktnx.mobileledger.async.AsyncCrasher;
45 import net.ktnx.mobileledger.async.DescriptionSelectedCallback;
46 import net.ktnx.mobileledger.async.SendTransactionTask;
47 import net.ktnx.mobileledger.async.TaskCallback;
48 import net.ktnx.mobileledger.databinding.ActivityNewTransactionBinding;
49 import net.ktnx.mobileledger.db.DB;
50 import net.ktnx.mobileledger.db.TemplateHeader;
51 import net.ktnx.mobileledger.model.Data;
52 import net.ktnx.mobileledger.model.LedgerTransaction;
53 import net.ktnx.mobileledger.model.MatchedTemplate;
54 import net.ktnx.mobileledger.ui.FabManager;
55 import net.ktnx.mobileledger.ui.QR;
56 import net.ktnx.mobileledger.ui.activity.ProfileThemedActivity;
57 import net.ktnx.mobileledger.ui.templates.TemplatesActivity;
58 import net.ktnx.mobileledger.utils.Logger;
59 import net.ktnx.mobileledger.utils.MLDB;
60 import net.ktnx.mobileledger.utils.Misc;
62 import java.util.ArrayList;
63 import java.util.List;
64 import java.util.Objects;
65 import java.util.regex.Matcher;
66 import java.util.regex.Pattern;
68 import static net.ktnx.mobileledger.utils.Logger.debug;
70 public class NewTransactionActivity extends ProfileThemedActivity
71 implements TaskCallback, NewTransactionFragment.OnNewTransactionFragmentInteractionListener,
72 QR.QRScanTrigger, QR.QRScanResultReceiver, DescriptionSelectedCallback,
73 FabManager.FabHandler {
74 private NavController navController;
75 private NewTransactionModel model;
76 private ActivityResultLauncher<Void> qrScanLauncher;
77 private ActivityNewTransactionBinding b;
78 private FabManager fabManager;
80 protected void onCreate(Bundle savedInstanceState) {
81 super.onCreate(savedInstanceState);
83 b = ActivityNewTransactionBinding.inflate(getLayoutInflater(), null, false);
84 setContentView(b.getRoot());
85 setSupportActionBar(b.toolbar);
86 Data.observeProfile(this,
87 mobileLedgerProfile -> b.toolbar.setSubtitle(mobileLedgerProfile.getName()));
89 NavHostFragment navHostFragment = (NavHostFragment) Objects.requireNonNull(
90 getSupportFragmentManager().findFragmentById(R.id.new_transaction_nav));
91 navController = navHostFragment.getNavController();
93 Objects.requireNonNull(getSupportActionBar())
94 .setDisplayHomeAsUpEnabled(true);
96 model = new ViewModelProvider(this).get(NewTransactionModel.class);
98 qrScanLauncher = QR.registerLauncher(this, this);
100 fabManager = new FabManager(b.fabAdd);
102 model.isSubmittable()
103 .observe(this, isSubmittable -> {
105 fabManager.showFab();
108 fabManager.hideFab();
111 // viewModel.checkTransactionSubmittable(listAdapter);
113 b.fabAdd.setOnClickListener(v -> onFabPressed());
116 protected void initProfile() {
117 String profileUUID = getIntent().getStringExtra("profile_uuid");
119 if (profileUUID != null) {
120 mProfile = Data.getProfile(profileUUID);
121 if (mProfile == null)
123 Data.setCurrentProfile(mProfile);
129 public void finish() {
131 overridePendingTransition(R.anim.dummy, R.anim.slide_out_down);
134 public boolean onOptionsItemSelected(MenuItem item) {
135 if (item.getItemId() == android.R.id.home) {
139 return super.onOptionsItemSelected(item);
141 public void onTransactionSave(LedgerTransaction tr) {
142 navController.navigate(R.id.action_newTransactionFragment_to_newTransactionSavingFragment);
145 SendTransactionTask saver =
146 new SendTransactionTask(this, mProfile, model.getSimulateSaveFlag());
149 catch (Exception e) {
150 debug("new-transaction", "Unknown error", e);
152 Bundle b = new Bundle();
153 b.putString("error", "unknown error");
154 navController.navigate(R.id.newTransactionFragment, b);
157 public void simulateCrash(MenuItem item) {
158 debug("crash", "Will crash intentionally");
159 new AsyncCrasher().execute();
161 public boolean onCreateOptionsMenu(Menu menu) {
162 // Inflate the menu; this adds items to the action bar if it is present.
163 getMenuInflater().inflate(R.menu.new_transaction, menu);
165 if (BuildConfig.DEBUG) {
166 menu.findItem(R.id.action_simulate_crash)
168 menu.findItem(R.id.action_simulate_save)
172 model.getSimulateSave()
173 .observe(this, state -> {
174 menu.findItem(R.id.action_simulate_save)
176 b.simulationLabel.setVisibility(state ? View.VISIBLE : View.GONE);
183 public int dp2px(float dp) {
184 return Math.round(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp,
185 getResources().getDisplayMetrics()));
188 public void done(String error) {
189 Bundle b = new Bundle();
191 b.putString("error", error);
192 navController.navigate(R.id.action_newTransactionSavingFragment_Failure, b);
195 navController.navigate(R.id.action_newTransactionSavingFragment_Success, b);
197 public void toggleSimulateSave(MenuItem item) {
198 model.toggleSimulateSave();
202 public void triggerQRScan() {
203 qrScanLauncher.launch(null);
205 private void startNewPatternActivity(String scanned) {
206 Intent intent = new Intent(this, TemplatesActivity.class);
207 Bundle args = new Bundle();
208 args.putString(TemplatesActivity.ARG_ADD_TEMPLATE, scanned);
209 startActivity(intent, args);
211 private void alertNoTemplateMatch(String scanned) {
212 MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(this);
213 builder.setCancelable(true)
214 .setMessage(R.string.no_template_matches)
215 .setPositiveButton(R.string.add_button,
216 (dialog, which) -> startNewPatternActivity(scanned))
220 public void onQRScanResult(String text) {
221 Logger.debug("qr", String.format("Got QR scan result [%s]", text));
223 if (Misc.emptyIsNull(text) == null)
226 LiveData<List<TemplateHeader>> allTemplates = DB.get()
229 allTemplates.observe(this, templateHeaders -> {
230 ArrayList<MatchedTemplate> matchingFallbackTemplates = new ArrayList<>();
231 ArrayList<MatchedTemplate> matchingTemplates = new ArrayList<>();
233 for (TemplateHeader ph : templateHeaders) {
234 String patternSource = ph.getRegularExpression();
235 if (Misc.emptyIsNull(patternSource) == null)
238 Pattern pattern = Pattern.compile(patternSource);
239 Matcher matcher = pattern.matcher(text);
240 if (!matcher.matches())
243 Logger.debug("pattern",
244 String.format("Pattern '%s' [%s] matches '%s'", ph.getName(),
245 patternSource, text));
247 matchingFallbackTemplates.add(
248 new MatchedTemplate(ph, matcher.toMatchResult()));
250 matchingTemplates.add(new MatchedTemplate(ph, matcher.toMatchResult()));
252 catch (ParcelFormatException e) {
254 Logger.debug("pattern",
255 String.format("Error compiling regular expression '%s'", patternSource),
260 if (matchingTemplates.isEmpty())
261 matchingTemplates = matchingFallbackTemplates;
263 if (matchingTemplates.isEmpty())
264 alertNoTemplateMatch(text);
265 else if (matchingTemplates.size() == 1)
266 model.applyTemplate(matchingTemplates.get(0), text);
268 chooseTemplate(matchingTemplates, text);
271 private void chooseTemplate(ArrayList<MatchedTemplate> matchingTemplates, String matchedText) {
272 final String templateNameColumn = "name";
273 AbstractCursor cursor = new AbstractCursor() {
275 public int getCount() {
276 return matchingTemplates.size();
279 public String[] getColumnNames() {
280 return new String[]{"_id", templateNameColumn};
283 public String getString(int column) {
285 return String.valueOf(getPosition());
286 return matchingTemplates.get(getPosition()).templateHead.getName();
289 public short getShort(int column) {
291 return (short) getPosition();
295 public int getInt(int column) {
296 return getShort(column);
299 public long getLong(int column) {
300 return getShort(column);
303 public float getFloat(int column) {
304 return getShort(column);
307 public double getDouble(int column) {
308 return getShort(column);
311 public boolean isNull(int column) {
315 public int getColumnCount() {
320 MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(this);
321 builder.setCancelable(true)
322 .setTitle(R.string.choose_template_to_apply)
323 .setIcon(R.drawable.ic_baseline_auto_graph_24)
324 .setSingleChoiceItems(cursor, -1, templateNameColumn, (dialog, which) -> {
325 model.applyTemplate(matchingTemplates.get(which), matchedText);
331 public void descriptionSelected(String description) {
332 debug("description selected", description);
333 if (!model.accountListIsEmpty())
336 String accFilter = mProfile.getPreferredAccountsFilter();
338 ArrayList<String> params = new ArrayList<>();
339 StringBuilder sb = new StringBuilder("select t.profile, t.id from transactions t");
341 if (!TextUtils.isEmpty(accFilter)) {
342 sb.append(" JOIN transaction_accounts ta")
343 .append(" ON ta.profile = t.profile")
344 .append(" AND ta.transaction_id = t.id");
347 sb.append(" WHERE t.description=?");
348 params.add(description);
350 if (!TextUtils.isEmpty(accFilter)) {
351 sb.append(" AND ta.account_name LIKE '%'||?||'%'");
352 params.add(accFilter);
355 sb.append(" ORDER BY t.year desc, t.month desc, t.day desc LIMIT 1");
357 final String sql = sb.toString();
358 debug("description", sql);
359 debug("description", params.toString());
361 // FIXME: handle exceptions?
362 MLDB.queryInBackground(sql, params.toArray(new String[]{}), new MLDB.CallbackHelper() {
364 public void onStart() {
365 model.incrementBusyCounter();
368 public void onDone() {
369 model.decrementBusyCounter();
372 public boolean onRow(@NonNull Cursor cursor) {
373 final String profileUUID = cursor.getString(0);
374 final int transactionId = cursor.getInt(1);
375 runOnUiThread(() -> model.loadTransactionIntoModel(profileUUID, transactionId));
376 return false; // limit 1, by the way
379 public void onNoRows() {
380 if (TextUtils.isEmpty(accFilter))
383 debug("description", "Trying transaction search without preferred account filter");
385 final String broaderSql =
386 "select t.profile, t.id from transactions t where t.description=?" +
387 " ORDER BY year desc, month desc, day desc LIMIT 1";
389 debug("description", broaderSql);
390 debug("description", description);
392 runOnUiThread(() -> Snackbar.make(b.newTransactionNav,
393 R.string.ignoring_preferred_account, Snackbar.LENGTH_INDEFINITE)
396 MLDB.queryInBackground(broaderSql, new String[]{description},
397 new MLDB.CallbackHelper() {
399 public void onStart() {
400 model.incrementBusyCounter();
403 public boolean onRow(@NonNull Cursor cursor) {
404 final String profileUUID = cursor.getString(0);
405 final int transactionId = cursor.getInt(1);
406 runOnUiThread(() -> model.loadTransactionIntoModel(profileUUID,
411 public void onDone() {
412 model.decrementBusyCounter();
418 private void onFabPressed() {
420 Misc.hideSoftKeyboard(this);
422 LedgerTransaction tr = model.constructLedgerTransaction();
424 onTransactionSave(tr);
427 public Context getContext() {
431 public void showManagedFab() {
432 if (Objects.requireNonNull(model.isSubmittable()
434 fabManager.showFab();
437 public void hideManagedFab() {
438 fabManager.hideFab();