X-Git-Url: https://git.ktnx.net/?a=blobdiff_plain;f=app%2Fsrc%2Fmain%2Fjava%2Fnet%2Fktnx%2Fmobileledger%2Fui%2Fnew_transaction%2FNewTransactionActivity.java;h=a1d9e74d904877bb800eebffd811e126f591e4ff;hb=2692a4d29cf595d6b171e018768d891d8481bf92;hp=413365db4f48e8db860a3e595d738f1da5206de8;hpb=9fad5003ac30c3e4f9d073e04f4569aeb31779b2;p=mobile-ledger.git diff --git a/app/src/main/java/net/ktnx/mobileledger/ui/new_transaction/NewTransactionActivity.java b/app/src/main/java/net/ktnx/mobileledger/ui/new_transaction/NewTransactionActivity.java index 413365db..a1d9e74d 100644 --- a/app/src/main/java/net/ktnx/mobileledger/ui/new_transaction/NewTransactionActivity.java +++ b/app/src/main/java/net/ktnx/mobileledger/ui/new_transaction/NewTransactionActivity.java @@ -17,43 +17,84 @@ package net.ktnx.mobileledger.ui.new_transaction; +import android.content.Context; +import android.content.Intent; +import android.database.AbstractCursor; import android.os.Bundle; +import android.os.ParcelFormatException; import android.util.TypedValue; import android.view.Menu; import android.view.MenuItem; import android.view.View; -import androidx.appcompat.widget.Toolbar; +import androidx.activity.result.ActivityResultLauncher; +import androidx.core.view.MenuCompat; +import androidx.lifecycle.LiveData; import androidx.lifecycle.ViewModelProvider; import androidx.navigation.NavController; import androidx.navigation.fragment.NavHostFragment; +import com.google.android.material.dialog.MaterialAlertDialogBuilder; + import net.ktnx.mobileledger.BuildConfig; import net.ktnx.mobileledger.R; -import net.ktnx.mobileledger.async.AsyncCrasher; +import net.ktnx.mobileledger.async.DescriptionSelectedCallback; +import net.ktnx.mobileledger.async.GeneralBackgroundTasks; import net.ktnx.mobileledger.async.SendTransactionTask; import net.ktnx.mobileledger.async.TaskCallback; +import net.ktnx.mobileledger.dao.BaseDAO; +import net.ktnx.mobileledger.dao.TransactionDAO; +import net.ktnx.mobileledger.databinding.ActivityNewTransactionBinding; +import net.ktnx.mobileledger.db.DB; +import net.ktnx.mobileledger.db.TemplateHeader; +import net.ktnx.mobileledger.db.TransactionWithAccounts; import net.ktnx.mobileledger.model.Data; import net.ktnx.mobileledger.model.LedgerTransaction; +import net.ktnx.mobileledger.model.MatchedTemplate; +import net.ktnx.mobileledger.ui.FabManager; +import net.ktnx.mobileledger.ui.QR; import net.ktnx.mobileledger.ui.activity.ProfileThemedActivity; +import net.ktnx.mobileledger.ui.activity.SplashActivity; +import net.ktnx.mobileledger.ui.templates.TemplatesActivity; +import net.ktnx.mobileledger.utils.Logger; +import net.ktnx.mobileledger.utils.Misc; +import java.util.ArrayList; +import java.util.List; import java.util.Objects; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import static net.ktnx.mobileledger.utils.Logger.debug; -public class NewTransactionActivity extends ProfileThemedActivity implements TaskCallback, - NewTransactionFragment.OnNewTransactionFragmentInteractionListener { +public class NewTransactionActivity extends ProfileThemedActivity + implements TaskCallback, NewTransactionFragment.OnNewTransactionFragmentInteractionListener, + QR.QRScanTrigger, QR.QRScanResultReceiver, DescriptionSelectedCallback, + FabManager.FabHandler { + final String TAG = "new-t-a"; private NavController navController; private NewTransactionModel model; + private ActivityResultLauncher qrScanLauncher; + private ActivityNewTransactionBinding b; + private FabManager fabManager; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - setContentView(R.layout.activity_new_transaction); - Toolbar toolbar = findViewById(R.id.toolbar); - setSupportActionBar(toolbar); - Data.observeProfile(this, - mobileLedgerProfile -> toolbar.setSubtitle(mobileLedgerProfile.getName())); + b = ActivityNewTransactionBinding.inflate(getLayoutInflater(), null, false); + setContentView(b.getRoot()); + setSupportActionBar(b.toolbar); + Data.observeProfile(this, profile -> { + if (profile == null) { + Logger.debug("new-t-act", "no active profile. Redirecting to SplashActivity"); + Intent intent = new Intent(this, SplashActivity.class); + intent.setFlags(Intent.FLAG_ACTIVITY_TASK_ON_HOME | Intent.FLAG_ACTIVITY_NEW_TASK); + startActivity(intent); + finish(); + } + else + b.toolbar.setSubtitle(profile.getName()); + }); NavHostFragment navHostFragment = (NavHostFragment) Objects.requireNonNull( getSupportFragmentManager().findFragmentById(R.id.new_transaction_nav)); @@ -63,19 +104,43 @@ public class NewTransactionActivity extends ProfileThemedActivity implements Tas .setDisplayHomeAsUpEnabled(true); model = new ViewModelProvider(this).get(NewTransactionModel.class); + + qrScanLauncher = QR.registerLauncher(this, this); + + fabManager = new FabManager(b.fabAdd); + + model.isSubmittable() + .observe(this, isSubmittable -> { + if (isSubmittable) { + fabManager.showFab(); + } + else { + fabManager.hideFab(); + } + }); +// viewModel.checkTransactionSubmittable(listAdapter); + + b.fabAdd.setOnClickListener(v -> onFabPressed()); } @Override protected void initProfile() { - String profileUUID = getIntent().getStringExtra("profile_uuid"); + long profileId = getIntent().getLongExtra(PARAM_PROFILE_ID, 0); + int profileHue = getIntent().getIntExtra(PARAM_THEME, -1); - if (profileUUID != null) { - mProfile = Data.getProfile(profileUUID); - if (mProfile == null) - finish(); - Data.setCurrentProfile(mProfile); + if (profileHue < 0) { + Logger.debug(TAG, "Started with invalid/missing theme; quitting"); + finish(); + return; } - else - super.initProfile(); + + if (profileId <= 0) { + Logger.debug(TAG, "Started with invalid/missing profile_id; quitting"); + finish(); + return; + } + + setupProfileColors(profileHue); + initProfile(profileId); } @Override public void finish() { @@ -95,37 +160,44 @@ public class NewTransactionActivity extends ProfileThemedActivity implements Tas try { SendTransactionTask saver = - new SendTransactionTask(this, mProfile, model.getSimulateSave()); - saver.execute(tr); + new SendTransactionTask(this, mProfile, tr, model.getSimulateSaveFlag()); + saver.start(); } catch (Exception e) { - debug("new-transaction", "Unknown error", e); + debug("new-transaction", "Unknown error: " + e); Bundle b = new Bundle(); b.putString("error", "unknown error"); navController.navigate(R.id.newTransactionFragment, b); } } - public void simulateCrash(MenuItem item) { + public boolean onSimulateCrashMenuItemClicked(MenuItem item) { debug("crash", "Will crash intentionally"); - new AsyncCrasher().execute(); + GeneralBackgroundTasks.run(() -> { throw new RuntimeException("Simulated crash");}); + return true; } public boolean onCreateOptionsMenu(Menu menu) { + super.onCreateOptionsMenu(menu); + + if (!BuildConfig.DEBUG) + return true; + // Inflate the menu; this adds items to the action bar if it is present. getMenuInflater().inflate(R.menu.new_transaction, menu); - if (BuildConfig.DEBUG) { - menu.findItem(R.id.action_simulate_crash) - .setVisible(true); - menu.findItem(R.id.action_simulate_save) - .setVisible(true); - } + MenuCompat.setGroupDividerEnabled(menu, true); - model.observeSimulateSave(this, state -> { - menu.findItem(R.id.action_simulate_save) - .setChecked(state); - findViewById(R.id.simulationLabel).setVisibility(state ? View.VISIBLE : View.GONE); - }); + menu.findItem(R.id.action_simulate_save) + .setOnMenuItemClickListener(this::onToggleSimulateSaveMenuItemClicked); + menu.findItem(R.id.action_simulate_crash) + .setOnMenuItemClickListener(this::onSimulateCrashMenuItemClicked); + + model.getSimulateSave() + .observe(this, state -> { + menu.findItem(R.id.action_simulate_save) + .setChecked(state); + b.simulationLabel.setVisibility(state ? View.VISIBLE : View.GONE); + }); return true; } @@ -136,17 +208,201 @@ public class NewTransactionActivity extends ProfileThemedActivity implements Tas getResources().getDisplayMetrics())); } @Override - public void done(String error) { + public void onTransactionSaveDone(String error, Object arg) { Bundle b = new Bundle(); if (error != null) { b.putString("error", error); navController.navigate(R.id.action_newTransactionSavingFragment_Failure, b); } - else + else { navController.navigate(R.id.action_newTransactionSavingFragment_Success, b); + + BaseDAO.runAsync(() -> commitToDb((LedgerTransaction) arg)); + } + } + public void commitToDb(LedgerTransaction tr) { + TransactionWithAccounts dbTransaction = tr.toDBO(); + DB.get() + .getTransactionDAO() + .appendSync(dbTransaction); } - public void toggleSimulateSave(MenuItem item) { + public boolean onToggleSimulateSaveMenuItemClicked(MenuItem item) { model.toggleSimulateSave(); + return true; + } + + @Override + public void triggerQRScan() { + qrScanLauncher.launch(null); } + private void startNewPatternActivity(String scanned) { + Intent intent = new Intent(this, TemplatesActivity.class); + Bundle args = new Bundle(); + args.putString(TemplatesActivity.ARG_ADD_TEMPLATE, scanned); + startActivity(intent, args); + } + private void alertNoTemplateMatch(String scanned) { + MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(this); + builder.setCancelable(true) + .setMessage(R.string.no_template_matches) + .setPositiveButton(R.string.add_button, + (dialog, which) -> startNewPatternActivity(scanned)) + .create() + .show(); + } + public void onQRScanResult(String text) { + Logger.debug("qr", String.format("Got QR scan result [%s]", text)); + + if (Misc.emptyIsNull(text) == null) + return; + + LiveData> allTemplates = DB.get() + .getTemplateDAO() + .getTemplates(); + allTemplates.observe(this, templateHeaders -> { + ArrayList matchingFallbackTemplates = new ArrayList<>(); + ArrayList matchingTemplates = new ArrayList<>(); + + for (TemplateHeader ph : templateHeaders) { + String patternSource = ph.getRegularExpression(); + if (Misc.emptyIsNull(patternSource) == null) + continue; + try { + Pattern pattern = Pattern.compile(patternSource); + Matcher matcher = pattern.matcher(text); + if (!matcher.matches()) + continue; + + Logger.debug("pattern", + String.format("Pattern '%s' [%s] matches '%s'", ph.getName(), + patternSource, text)); + if (ph.isFallback()) + matchingFallbackTemplates.add( + new MatchedTemplate(ph, matcher.toMatchResult())); + else + matchingTemplates.add(new MatchedTemplate(ph, matcher.toMatchResult())); + } + catch (ParcelFormatException e) { + // ignored + Logger.debug("pattern", + String.format("Error compiling regular expression '%s'", patternSource), + e); + } + } + + if (matchingTemplates.isEmpty()) + matchingTemplates = matchingFallbackTemplates; + + if (matchingTemplates.isEmpty()) + alertNoTemplateMatch(text); + else if (matchingTemplates.size() == 1) + model.applyTemplate(matchingTemplates.get(0), text); + else + chooseTemplate(matchingTemplates, text); + }); + } + private void chooseTemplate(ArrayList matchingTemplates, String matchedText) { + final String templateNameColumn = "name"; + AbstractCursor cursor = new AbstractCursor() { + @Override + public int getCount() { + return matchingTemplates.size(); + } + @Override + public String[] getColumnNames() { + return new String[]{"_id", templateNameColumn}; + } + @Override + public String getString(int column) { + if (column == 0) + return String.valueOf(getPosition()); + return matchingTemplates.get(getPosition()).templateHead.getName(); + } + @Override + public short getShort(int column) { + if (column == 0) + return (short) getPosition(); + return -1; + } + @Override + public int getInt(int column) { + return getShort(column); + } + @Override + public long getLong(int column) { + return getShort(column); + } + @Override + public float getFloat(int column) { + return getShort(column); + } + @Override + public double getDouble(int column) { + return getShort(column); + } + @Override + public boolean isNull(int column) { + return false; + } + @Override + public int getColumnCount() { + return 2; + } + }; + + MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(this); + builder.setCancelable(true) + .setTitle(R.string.choose_template_to_apply) + .setIcon(R.drawable.ic_baseline_auto_graph_24) + .setSingleChoiceItems(cursor, -1, templateNameColumn, (dialog, which) -> { + model.applyTemplate(matchingTemplates.get(which), matchedText); + dialog.dismiss(); + }) + .create() + .show(); + } + public void onDescriptionSelected(String description) { + debug("description selected", description); + if (!model.accountListIsEmpty()) + return; + + BaseDAO.runAsync(() -> { + String accFilter = mProfile.getPreferredAccountsFilter(); + + TransactionDAO trDao = DB.get() + .getTransactionDAO(); + TransactionWithAccounts tr = null; + + if (Misc.emptyIsNull(accFilter) != null) + tr = trDao.getFirstByDescriptionHavingAccountSync(description, accFilter); + if (tr == null) + tr = trDao.getFirstByDescriptionSync(description); + + if (tr != null) + model.loadTransactionIntoModel(tr); + }); + } + private void onFabPressed() { + fabManager.hideFab(); + Misc.hideSoftKeyboard(this); + + LedgerTransaction tr = model.constructLedgerTransaction(); + + onTransactionSave(tr); + } + @Override + public Context getContext() { + return this; + } + @Override + public void showManagedFab() { + if (Objects.requireNonNull(model.isSubmittable() + .getValue())) + fabManager.showFab(); + } + @Override + public void hideManagedFab() { + fabManager.hideFab(); + } }