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.os.Bundle;
24 import android.os.ParcelFormatException;
25 import android.util.TypedValue;
26 import android.view.Menu;
27 import android.view.MenuItem;
28 import android.view.View;
30 import androidx.activity.result.ActivityResultLauncher;
31 import androidx.core.view.MenuCompat;
32 import androidx.lifecycle.LiveData;
33 import androidx.lifecycle.ViewModelProvider;
34 import androidx.navigation.NavController;
35 import androidx.navigation.fragment.NavHostFragment;
37 import com.google.android.material.dialog.MaterialAlertDialogBuilder;
39 import net.ktnx.mobileledger.BuildConfig;
40 import net.ktnx.mobileledger.R;
41 import net.ktnx.mobileledger.async.DescriptionSelectedCallback;
42 import net.ktnx.mobileledger.async.GeneralBackgroundTasks;
43 import net.ktnx.mobileledger.async.SendTransactionTask;
44 import net.ktnx.mobileledger.async.TaskCallback;
45 import net.ktnx.mobileledger.dao.BaseDAO;
46 import net.ktnx.mobileledger.dao.TransactionDAO;
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.db.TransactionWithAccounts;
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.activity.SplashActivity;
58 import net.ktnx.mobileledger.ui.templates.TemplatesActivity;
59 import net.ktnx.mobileledger.utils.Logger;
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 final String TAG = "new-t-a";
75 private NavController navController;
76 private NewTransactionModel model;
77 private ActivityResultLauncher<Void> qrScanLauncher;
78 private ActivityNewTransactionBinding b;
79 private FabManager fabManager;
81 protected void onCreate(Bundle savedInstanceState) {
82 super.onCreate(savedInstanceState);
84 b = ActivityNewTransactionBinding.inflate(getLayoutInflater(), null, false);
85 setContentView(b.getRoot());
86 setSupportActionBar(b.toolbar);
87 Data.observeProfile(this, profile -> {
88 if (profile == null) {
89 Logger.debug("new-t-act", "no active profile. Redirecting to SplashActivity");
90 Intent intent = new Intent(this, SplashActivity.class);
91 intent.setFlags(Intent.FLAG_ACTIVITY_TASK_ON_HOME | Intent.FLAG_ACTIVITY_NEW_TASK);
92 startActivity(intent);
96 b.toolbar.setSubtitle(profile.getName());
99 NavHostFragment navHostFragment = (NavHostFragment) Objects.requireNonNull(
100 getSupportFragmentManager().findFragmentById(R.id.new_transaction_nav));
101 navController = navHostFragment.getNavController();
103 Objects.requireNonNull(getSupportActionBar())
104 .setDisplayHomeAsUpEnabled(true);
106 model = new ViewModelProvider(this).get(NewTransactionModel.class);
108 qrScanLauncher = QR.registerLauncher(this, this);
110 fabManager = new FabManager(b.fabAdd);
112 model.isSubmittable()
113 .observe(this, isSubmittable -> {
115 fabManager.showFab();
118 fabManager.hideFab();
121 // viewModel.checkTransactionSubmittable(listAdapter);
123 b.fabAdd.setOnClickListener(v -> onFabPressed());
126 protected void initProfile() {
127 long profileId = getIntent().getLongExtra(PARAM_PROFILE_ID, 0);
128 int profileHue = getIntent().getIntExtra(PARAM_THEME, -1);
130 if (profileHue < 0) {
131 Logger.debug(TAG, "Started with invalid/missing theme; quitting");
136 if (profileId <= 0) {
137 Logger.debug(TAG, "Started with invalid/missing profile_id; quitting");
142 setupProfileColors(profileHue);
143 initProfile(profileId);
146 public void finish() {
148 overridePendingTransition(R.anim.dummy, R.anim.slide_out_down);
151 public boolean onOptionsItemSelected(MenuItem item) {
152 if (item.getItemId() == android.R.id.home) {
156 return super.onOptionsItemSelected(item);
158 public void onTransactionSave(LedgerTransaction tr) {
159 navController.navigate(R.id.action_newTransactionFragment_to_newTransactionSavingFragment);
162 SendTransactionTask saver =
163 new SendTransactionTask(this, mProfile, tr, model.getSimulateSaveFlag());
166 catch (Exception e) {
167 debug("new-transaction", "Unknown error: " + e);
169 Bundle b = new Bundle();
170 b.putString("error", "unknown error");
171 navController.navigate(R.id.newTransactionFragment, b);
174 public boolean onSimulateCrashMenuItemClicked(MenuItem item) {
175 debug("crash", "Will crash intentionally");
176 GeneralBackgroundTasks.run(() -> { throw new RuntimeException("Simulated crash");});
179 public boolean onCreateOptionsMenu(Menu menu) {
180 super.onCreateOptionsMenu(menu);
182 if (!BuildConfig.DEBUG)
185 // Inflate the menu; this adds items to the action bar if it is present.
186 getMenuInflater().inflate(R.menu.new_transaction, menu);
188 MenuCompat.setGroupDividerEnabled(menu, true);
190 menu.findItem(R.id.action_simulate_save)
191 .setOnMenuItemClickListener(this::onToggleSimulateSaveMenuItemClicked);
192 menu.findItem(R.id.action_simulate_crash)
193 .setOnMenuItemClickListener(this::onSimulateCrashMenuItemClicked);
195 model.getSimulateSave()
196 .observe(this, state -> {
197 menu.findItem(R.id.action_simulate_save)
199 b.simulationLabel.setVisibility(state ? View.VISIBLE : View.GONE);
206 public int dp2px(float dp) {
207 return Math.round(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp,
208 getResources().getDisplayMetrics()));
211 public void onTransactionSaveDone(String error, Object arg) {
212 Bundle b = new Bundle();
214 b.putString("error", error);
215 navController.navigate(R.id.action_newTransactionSavingFragment_Failure, b);
218 navController.navigate(R.id.action_newTransactionSavingFragment_Success, b);
220 BaseDAO.runAsync(() -> commitToDb((LedgerTransaction) arg));
223 public void commitToDb(LedgerTransaction tr) {
224 TransactionWithAccounts dbTransaction = tr.toDBO();
227 .appendSync(dbTransaction);
229 public boolean onToggleSimulateSaveMenuItemClicked(MenuItem item) {
230 model.toggleSimulateSave();
235 public void triggerQRScan() {
236 qrScanLauncher.launch(null);
238 private void startNewPatternActivity(String scanned) {
239 Intent intent = new Intent(this, TemplatesActivity.class);
240 Bundle args = new Bundle();
241 args.putString(TemplatesActivity.ARG_ADD_TEMPLATE, scanned);
242 startActivity(intent, args);
244 private void alertNoTemplateMatch(String scanned) {
245 MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(this);
246 builder.setCancelable(true)
247 .setMessage(R.string.no_template_matches)
248 .setPositiveButton(R.string.add_button,
249 (dialog, which) -> startNewPatternActivity(scanned))
253 public void onQRScanResult(String text) {
254 Logger.debug("qr", String.format("Got QR scan result [%s]", text));
256 if (Misc.emptyIsNull(text) == null)
259 LiveData<List<TemplateHeader>> allTemplates = DB.get()
262 allTemplates.observe(this, templateHeaders -> {
263 ArrayList<MatchedTemplate> matchingFallbackTemplates = new ArrayList<>();
264 ArrayList<MatchedTemplate> matchingTemplates = new ArrayList<>();
266 for (TemplateHeader ph : templateHeaders) {
267 String patternSource = ph.getRegularExpression();
268 if (Misc.emptyIsNull(patternSource) == null)
271 Pattern pattern = Pattern.compile(patternSource);
272 Matcher matcher = pattern.matcher(text);
273 if (!matcher.matches())
276 Logger.debug("pattern",
277 String.format("Pattern '%s' [%s] matches '%s'", ph.getName(),
278 patternSource, text));
280 matchingFallbackTemplates.add(
281 new MatchedTemplate(ph, matcher.toMatchResult()));
283 matchingTemplates.add(new MatchedTemplate(ph, matcher.toMatchResult()));
285 catch (ParcelFormatException e) {
287 Logger.debug("pattern",
288 String.format("Error compiling regular expression '%s'", patternSource),
293 if (matchingTemplates.isEmpty())
294 matchingTemplates = matchingFallbackTemplates;
296 if (matchingTemplates.isEmpty())
297 alertNoTemplateMatch(text);
298 else if (matchingTemplates.size() == 1)
299 model.applyTemplate(matchingTemplates.get(0), text);
301 chooseTemplate(matchingTemplates, text);
304 private void chooseTemplate(ArrayList<MatchedTemplate> matchingTemplates, String matchedText) {
305 final String templateNameColumn = "name";
306 AbstractCursor cursor = new AbstractCursor() {
308 public int getCount() {
309 return matchingTemplates.size();
312 public String[] getColumnNames() {
313 return new String[]{"_id", templateNameColumn};
316 public String getString(int column) {
318 return String.valueOf(getPosition());
319 return matchingTemplates.get(getPosition()).templateHead.getName();
322 public short getShort(int column) {
324 return (short) getPosition();
328 public int getInt(int column) {
329 return getShort(column);
332 public long getLong(int column) {
333 return getShort(column);
336 public float getFloat(int column) {
337 return getShort(column);
340 public double getDouble(int column) {
341 return getShort(column);
344 public boolean isNull(int column) {
348 public int getColumnCount() {
353 MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(this);
354 builder.setCancelable(true)
355 .setTitle(R.string.choose_template_to_apply)
356 .setIcon(R.drawable.ic_baseline_auto_graph_24)
357 .setSingleChoiceItems(cursor, -1, templateNameColumn, (dialog, which) -> {
358 model.applyTemplate(matchingTemplates.get(which), matchedText);
364 public void onDescriptionSelected(String description) {
365 debug("description selected", description);
366 if (!model.accountListIsEmpty())
369 BaseDAO.runAsync(() -> {
370 String accFilter = mProfile.getPreferredAccountsFilter();
372 TransactionDAO trDao = DB.get()
373 .getTransactionDAO();
375 TransactionWithAccounts tr = null;
377 if (Misc.emptyIsNull(accFilter) != null)
378 tr = trDao.getFirstByDescriptionHavingAccountSync(description, accFilter);
380 tr = trDao.getFirstByDescriptionSync(description);
383 model.loadTransactionIntoModel(tr);
386 private void onFabPressed() {
387 fabManager.hideFab();
388 Misc.hideSoftKeyboard(this);
390 LedgerTransaction tr = model.constructLedgerTransaction();
392 onTransactionSave(tr);
395 public Context getContext() {
399 public void showManagedFab() {
400 if (Objects.requireNonNull(model.isSubmittable()
402 fabManager.showFab();
405 public void hideManagedFab() {
406 fabManager.hideFab();