]> git.ktnx.net Git - mobile-ledger.git/blob - app/src/main/java/net/ktnx/mobileledger/ui/new_transaction/NewTransactionActivity.java
stop resetting the date when an old transaction is loaded into the form
[mobile-ledger.git] / app / src / main / java / net / ktnx / mobileledger / ui / new_transaction / NewTransactionActivity.java
1 /*
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.
8  *
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.
13  *
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/>.
16  */
17
18 package net.ktnx.mobileledger.ui.new_transaction;
19
20 import android.content.Context;
21 import android.content.Intent;
22 import android.database.AbstractCursor;
23 import android.os.AsyncTask;
24 import android.os.Bundle;
25 import android.os.ParcelFormatException;
26 import android.util.TypedValue;
27 import android.view.Menu;
28 import android.view.MenuItem;
29 import android.view.View;
30
31 import androidx.activity.result.ActivityResultLauncher;
32 import androidx.core.view.MenuCompat;
33 import androidx.lifecycle.LiveData;
34 import androidx.lifecycle.ViewModelProvider;
35 import androidx.navigation.NavController;
36 import androidx.navigation.fragment.NavHostFragment;
37
38 import com.google.android.material.dialog.MaterialAlertDialogBuilder;
39
40 import net.ktnx.mobileledger.BuildConfig;
41 import net.ktnx.mobileledger.R;
42 import net.ktnx.mobileledger.async.AsyncCrasher;
43 import net.ktnx.mobileledger.async.DescriptionSelectedCallback;
44 import net.ktnx.mobileledger.async.SendTransactionTask;
45 import net.ktnx.mobileledger.async.TaskCallback;
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.templates.TemplatesActivity;
58 import net.ktnx.mobileledger.utils.Logger;
59 import net.ktnx.mobileledger.utils.Misc;
60
61 import java.util.ArrayList;
62 import java.util.List;
63 import java.util.Objects;
64 import java.util.regex.Matcher;
65 import java.util.regex.Pattern;
66
67 import static net.ktnx.mobileledger.utils.Logger.debug;
68
69 public class NewTransactionActivity extends ProfileThemedActivity
70         implements TaskCallback, NewTransactionFragment.OnNewTransactionFragmentInteractionListener,
71         QR.QRScanTrigger, QR.QRScanResultReceiver, DescriptionSelectedCallback,
72         FabManager.FabHandler {
73     final String TAG = "new-t-a";
74     private NavController navController;
75     private NewTransactionModel model;
76     private ActivityResultLauncher<Void> qrScanLauncher;
77     private ActivityNewTransactionBinding b;
78     private FabManager fabManager;
79     @Override
80     protected void onCreate(Bundle savedInstanceState) {
81         super.onCreate(savedInstanceState);
82
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()));
88
89         NavHostFragment navHostFragment = (NavHostFragment) Objects.requireNonNull(
90                 getSupportFragmentManager().findFragmentById(R.id.new_transaction_nav));
91         navController = navHostFragment.getNavController();
92
93         Objects.requireNonNull(getSupportActionBar())
94                .setDisplayHomeAsUpEnabled(true);
95
96         model = new ViewModelProvider(this).get(NewTransactionModel.class);
97
98         qrScanLauncher = QR.registerLauncher(this, this);
99
100         fabManager = new FabManager(b.fabAdd);
101
102         model.isSubmittable()
103              .observe(this, isSubmittable -> {
104                  if (isSubmittable) {
105                      fabManager.showFab();
106                  }
107                  else {
108                      fabManager.hideFab();
109                  }
110              });
111 //        viewModel.checkTransactionSubmittable(listAdapter);
112
113         b.fabAdd.setOnClickListener(v -> onFabPressed());
114     }
115     @Override
116     protected void initProfile() {
117         long profileId = getIntent().getLongExtra(PARAM_PROFILE_ID, 0);
118         int profileHue = getIntent().getIntExtra(PARAM_THEME, -1);
119
120         if (profileHue < 0) {
121             Logger.debug(TAG, "Started with invalid/missing theme; quitting");
122             finish();
123             return;
124         }
125
126         if (profileId <= 0) {
127             Logger.debug(TAG, "Started with invalid/missing profile_id; quitting");
128             finish();
129             return;
130         }
131
132         setupProfileColors(profileHue);
133         initProfile(profileId);
134     }
135     @Override
136     public void finish() {
137         super.finish();
138         overridePendingTransition(R.anim.dummy, R.anim.slide_out_down);
139     }
140     @Override
141     public boolean onOptionsItemSelected(MenuItem item) {
142         if (item.getItemId() == android.R.id.home) {
143             finish();
144             return true;
145         }
146         return super.onOptionsItemSelected(item);
147     }
148     public void onTransactionSave(LedgerTransaction tr) {
149         navController.navigate(R.id.action_newTransactionFragment_to_newTransactionSavingFragment);
150         try {
151
152             SendTransactionTask saver =
153                     new SendTransactionTask(this, mProfile, model.getSimulateSaveFlag());
154             saver.execute(tr);
155         }
156         catch (Exception e) {
157             debug("new-transaction", "Unknown error: " + e);
158
159             Bundle b = new Bundle();
160             b.putString("error", "unknown error");
161             navController.navigate(R.id.newTransactionFragment, b);
162         }
163     }
164     public boolean onSimulateCrashMenuItemClicked(MenuItem item) {
165         debug("crash", "Will crash intentionally");
166         new AsyncCrasher().execute();
167         return true;
168     }
169     public boolean onCreateOptionsMenu(Menu menu) {
170         super.onCreateOptionsMenu(menu);
171
172         if (!BuildConfig.DEBUG)
173             return true;
174
175         // Inflate the menu; this adds items to the action bar if it is present.
176         getMenuInflater().inflate(R.menu.new_transaction, menu);
177
178         MenuCompat.setGroupDividerEnabled(menu, true);
179
180         menu.findItem(R.id.action_simulate_save)
181             .setOnMenuItemClickListener(this::onToggleSimulateSaveMenuItemClicked);
182         menu.findItem(R.id.action_simulate_crash)
183             .setOnMenuItemClickListener(this::onSimulateCrashMenuItemClicked);
184
185         model.getSimulateSave()
186              .observe(this, state -> {
187                  menu.findItem(R.id.action_simulate_save)
188                      .setChecked(state);
189                  b.simulationLabel.setVisibility(state ? View.VISIBLE : View.GONE);
190              });
191
192         return true;
193     }
194
195
196     public int dp2px(float dp) {
197         return Math.round(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp,
198                 getResources().getDisplayMetrics()));
199     }
200     @Override
201     public void done(String error, Object arg) {
202         Bundle b = new Bundle();
203         if (error != null) {
204             b.putString("error", error);
205             navController.navigate(R.id.action_newTransactionSavingFragment_Failure, b);
206         }
207         else {
208             navController.navigate(R.id.action_newTransactionSavingFragment_Success, b);
209
210             AsyncTask.execute(() -> commitToDb((LedgerTransaction) arg));
211         }
212     }
213     public void commitToDb(LedgerTransaction tr) {
214         TransactionWithAccounts dbTransaction = tr.toDBO();
215         DB.get()
216           .getTransactionDAO()
217           .appendSync(dbTransaction);
218     }
219     public boolean onToggleSimulateSaveMenuItemClicked(MenuItem item) {
220         model.toggleSimulateSave();
221         return true;
222     }
223
224     @Override
225     public void triggerQRScan() {
226         qrScanLauncher.launch(null);
227     }
228     private void startNewPatternActivity(String scanned) {
229         Intent intent = new Intent(this, TemplatesActivity.class);
230         Bundle args = new Bundle();
231         args.putString(TemplatesActivity.ARG_ADD_TEMPLATE, scanned);
232         startActivity(intent, args);
233     }
234     private void alertNoTemplateMatch(String scanned) {
235         MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(this);
236         builder.setCancelable(true)
237                .setMessage(R.string.no_template_matches)
238                .setPositiveButton(R.string.add_button,
239                        (dialog, which) -> startNewPatternActivity(scanned))
240                .create()
241                .show();
242     }
243     public void onQRScanResult(String text) {
244         Logger.debug("qr", String.format("Got QR scan result [%s]", text));
245
246         if (Misc.emptyIsNull(text) == null)
247             return;
248
249         LiveData<List<TemplateHeader>> allTemplates = DB.get()
250                                                         .getTemplateDAO()
251                                                         .getTemplates();
252         allTemplates.observe(this, templateHeaders -> {
253             ArrayList<MatchedTemplate> matchingFallbackTemplates = new ArrayList<>();
254             ArrayList<MatchedTemplate> matchingTemplates = new ArrayList<>();
255
256             for (TemplateHeader ph : templateHeaders) {
257                 String patternSource = ph.getRegularExpression();
258                 if (Misc.emptyIsNull(patternSource) == null)
259                     continue;
260                 try {
261                     Pattern pattern = Pattern.compile(patternSource);
262                     Matcher matcher = pattern.matcher(text);
263                     if (!matcher.matches())
264                         continue;
265
266                     Logger.debug("pattern",
267                             String.format("Pattern '%s' [%s] matches '%s'", ph.getName(),
268                                     patternSource, text));
269                     if (ph.isFallback())
270                         matchingFallbackTemplates.add(
271                                 new MatchedTemplate(ph, matcher.toMatchResult()));
272                     else
273                         matchingTemplates.add(new MatchedTemplate(ph, matcher.toMatchResult()));
274                 }
275                 catch (ParcelFormatException e) {
276                     // ignored
277                     Logger.debug("pattern",
278                             String.format("Error compiling regular expression '%s'", patternSource),
279                             e);
280                 }
281             }
282
283             if (matchingTemplates.isEmpty())
284                 matchingTemplates = matchingFallbackTemplates;
285
286             if (matchingTemplates.isEmpty())
287                 alertNoTemplateMatch(text);
288             else if (matchingTemplates.size() == 1)
289                 model.applyTemplate(matchingTemplates.get(0), text);
290             else
291                 chooseTemplate(matchingTemplates, text);
292         });
293     }
294     private void chooseTemplate(ArrayList<MatchedTemplate> matchingTemplates, String matchedText) {
295         final String templateNameColumn = "name";
296         AbstractCursor cursor = new AbstractCursor() {
297             @Override
298             public int getCount() {
299                 return matchingTemplates.size();
300             }
301             @Override
302             public String[] getColumnNames() {
303                 return new String[]{"_id", templateNameColumn};
304             }
305             @Override
306             public String getString(int column) {
307                 if (column == 0)
308                     return String.valueOf(getPosition());
309                 return matchingTemplates.get(getPosition()).templateHead.getName();
310             }
311             @Override
312             public short getShort(int column) {
313                 if (column == 0)
314                     return (short) getPosition();
315                 return -1;
316             }
317             @Override
318             public int getInt(int column) {
319                 return getShort(column);
320             }
321             @Override
322             public long getLong(int column) {
323                 return getShort(column);
324             }
325             @Override
326             public float getFloat(int column) {
327                 return getShort(column);
328             }
329             @Override
330             public double getDouble(int column) {
331                 return getShort(column);
332             }
333             @Override
334             public boolean isNull(int column) {
335                 return false;
336             }
337             @Override
338             public int getColumnCount() {
339                 return 2;
340             }
341         };
342
343         MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(this);
344         builder.setCancelable(true)
345                .setTitle(R.string.choose_template_to_apply)
346                .setIcon(R.drawable.ic_baseline_auto_graph_24)
347                .setSingleChoiceItems(cursor, -1, templateNameColumn, (dialog, which) -> {
348                    model.applyTemplate(matchingTemplates.get(which), matchedText);
349                    dialog.dismiss();
350                })
351                .create()
352                .show();
353     }
354     public void onDescriptionSelected(String description) {
355         debug("description selected", description);
356         if (!model.accountListIsEmpty())
357             return;
358
359         AsyncTask.execute(() -> {
360             String accFilter = mProfile.getPreferredAccountsFilter();
361
362             TransactionDAO trDao = DB.get()
363                                      .getTransactionDAO();
364
365             TransactionWithAccounts tr;
366
367             if (Misc.emptyIsNull(accFilter) != null) {
368                 tr = trDao.getFirstByDescriptionHavingAccountSync(description, accFilter);
369                 if (tr != null) {
370                     model.loadTransactionIntoModel(tr);
371                     return;
372                 }
373             }
374
375             tr = trDao.getFirstByDescriptionSync(description);
376             if (tr != null)
377                 model.loadTransactionIntoModel(tr);
378         });
379     }
380     private void onFabPressed() {
381         fabManager.hideFab();
382         Misc.hideSoftKeyboard(this);
383
384         LedgerTransaction tr = model.constructLedgerTransaction();
385
386         onTransactionSave(tr);
387     }
388     @Override
389     public Context getContext() {
390         return this;
391     }
392     @Override
393     public void showManagedFab() {
394         if (Objects.requireNonNull(model.isSubmittable()
395                                         .getValue()))
396             fabManager.showFab();
397     }
398     @Override
399     public void hideManagedFab() {
400         fabManager.hideFab();
401     }
402 }