]> git.ktnx.net Git - mobile-ledger.git/blob - app/src/main/java/net/ktnx/mobileledger/ui/new_transaction/NewTransactionActivity.java
switch new transaction UI to FabManager
[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.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;
31
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;
38
39 import com.google.android.material.dialog.MaterialAlertDialogBuilder;
40 import com.google.android.material.snackbar.Snackbar;
41
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;
61
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;
67
68 import static net.ktnx.mobileledger.utils.Logger.debug;
69
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;
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         String profileUUID = getIntent().getStringExtra("profile_uuid");
118
119         if (profileUUID != null) {
120             mProfile = Data.getProfile(profileUUID);
121             if (mProfile == null)
122                 finish();
123             Data.setCurrentProfile(mProfile);
124         }
125         else
126             super.initProfile();
127     }
128     @Override
129     public void finish() {
130         super.finish();
131         overridePendingTransition(R.anim.dummy, R.anim.slide_out_down);
132     }
133     @Override
134     public boolean onOptionsItemSelected(MenuItem item) {
135         if (item.getItemId() == android.R.id.home) {
136             finish();
137             return true;
138         }
139         return super.onOptionsItemSelected(item);
140     }
141     public void onTransactionSave(LedgerTransaction tr) {
142         navController.navigate(R.id.action_newTransactionFragment_to_newTransactionSavingFragment);
143         try {
144
145             SendTransactionTask saver =
146                     new SendTransactionTask(this, mProfile, model.getSimulateSaveFlag());
147             saver.execute(tr);
148         }
149         catch (Exception e) {
150             debug("new-transaction", "Unknown error", e);
151
152             Bundle b = new Bundle();
153             b.putString("error", "unknown error");
154             navController.navigate(R.id.newTransactionFragment, b);
155         }
156     }
157     public void simulateCrash(MenuItem item) {
158         debug("crash", "Will crash intentionally");
159         new AsyncCrasher().execute();
160     }
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);
164
165         if (BuildConfig.DEBUG) {
166             menu.findItem(R.id.action_simulate_crash)
167                 .setVisible(true);
168             menu.findItem(R.id.action_simulate_save)
169                 .setVisible(true);
170         }
171
172         model.getSimulateSave()
173              .observe(this, state -> {
174                  menu.findItem(R.id.action_simulate_save)
175                      .setChecked(state);
176                  b.simulationLabel.setVisibility(state ? View.VISIBLE : View.GONE);
177              });
178
179         return true;
180     }
181
182
183     public int dp2px(float dp) {
184         return Math.round(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp,
185                 getResources().getDisplayMetrics()));
186     }
187     @Override
188     public void done(String error) {
189         Bundle b = new Bundle();
190         if (error != null) {
191             b.putString("error", error);
192             navController.navigate(R.id.action_newTransactionSavingFragment_Failure, b);
193         }
194         else
195             navController.navigate(R.id.action_newTransactionSavingFragment_Success, b);
196     }
197     public void toggleSimulateSave(MenuItem item) {
198         model.toggleSimulateSave();
199     }
200
201     @Override
202     public void triggerQRScan() {
203         qrScanLauncher.launch(null);
204     }
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);
210     }
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))
217                .create()
218                .show();
219     }
220     public void onQRScanResult(String text) {
221         Logger.debug("qr", String.format("Got QR scan result [%s]", text));
222
223         if (Misc.emptyIsNull(text) == null)
224             return;
225
226         LiveData<List<TemplateHeader>> allTemplates = DB.get()
227                                                         .getTemplateDAO()
228                                                         .getTemplates();
229         allTemplates.observe(this, templateHeaders -> {
230             ArrayList<MatchedTemplate> matchingFallbackTemplates = new ArrayList<>();
231             ArrayList<MatchedTemplate> matchingTemplates = new ArrayList<>();
232
233             for (TemplateHeader ph : templateHeaders) {
234                 String patternSource = ph.getRegularExpression();
235                 if (Misc.emptyIsNull(patternSource) == null)
236                     continue;
237                 try {
238                     Pattern pattern = Pattern.compile(patternSource);
239                     Matcher matcher = pattern.matcher(text);
240                     if (!matcher.matches())
241                         continue;
242
243                     Logger.debug("pattern",
244                             String.format("Pattern '%s' [%s] matches '%s'", ph.getName(),
245                                     patternSource, text));
246                     if (ph.isFallback())
247                         matchingFallbackTemplates.add(
248                                 new MatchedTemplate(ph, matcher.toMatchResult()));
249                     else
250                         matchingTemplates.add(new MatchedTemplate(ph, matcher.toMatchResult()));
251                 }
252                 catch (ParcelFormatException e) {
253                     // ignored
254                     Logger.debug("pattern",
255                             String.format("Error compiling regular expression '%s'", patternSource),
256                             e);
257                 }
258             }
259
260             if (matchingTemplates.isEmpty())
261                 matchingTemplates = matchingFallbackTemplates;
262
263             if (matchingTemplates.isEmpty())
264                 alertNoTemplateMatch(text);
265             else if (matchingTemplates.size() == 1)
266                 model.applyTemplate(matchingTemplates.get(0), text);
267             else
268                 chooseTemplate(matchingTemplates, text);
269         });
270     }
271     private void chooseTemplate(ArrayList<MatchedTemplate> matchingTemplates, String matchedText) {
272         final String templateNameColumn = "name";
273         AbstractCursor cursor = new AbstractCursor() {
274             @Override
275             public int getCount() {
276                 return matchingTemplates.size();
277             }
278             @Override
279             public String[] getColumnNames() {
280                 return new String[]{"_id", templateNameColumn};
281             }
282             @Override
283             public String getString(int column) {
284                 if (column == 0)
285                     return String.valueOf(getPosition());
286                 return matchingTemplates.get(getPosition()).templateHead.getName();
287             }
288             @Override
289             public short getShort(int column) {
290                 if (column == 0)
291                     return (short) getPosition();
292                 return -1;
293             }
294             @Override
295             public int getInt(int column) {
296                 return getShort(column);
297             }
298             @Override
299             public long getLong(int column) {
300                 return getShort(column);
301             }
302             @Override
303             public float getFloat(int column) {
304                 return getShort(column);
305             }
306             @Override
307             public double getDouble(int column) {
308                 return getShort(column);
309             }
310             @Override
311             public boolean isNull(int column) {
312                 return false;
313             }
314             @Override
315             public int getColumnCount() {
316                 return 2;
317             }
318         };
319
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);
326                    dialog.dismiss();
327                })
328                .create()
329                .show();
330     }
331     public void descriptionSelected(String description) {
332         debug("description selected", description);
333         if (!model.accountListIsEmpty())
334             return;
335
336         String accFilter = mProfile.getPreferredAccountsFilter();
337
338         ArrayList<String> params = new ArrayList<>();
339         StringBuilder sb = new StringBuilder("select t.profile, t.id from transactions t");
340
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");
345         }
346
347         sb.append(" WHERE t.description=?");
348         params.add(description);
349
350         if (!TextUtils.isEmpty(accFilter)) {
351             sb.append(" AND ta.account_name LIKE '%'||?||'%'");
352             params.add(accFilter);
353         }
354
355         sb.append(" ORDER BY t.year desc, t.month desc, t.day desc LIMIT 1");
356
357         final String sql = sb.toString();
358         debug("description", sql);
359         debug("description", params.toString());
360
361         // FIXME: handle exceptions?
362         MLDB.queryInBackground(sql, params.toArray(new String[]{}), new MLDB.CallbackHelper() {
363             @Override
364             public void onStart() {
365                 model.incrementBusyCounter();
366             }
367             @Override
368             public void onDone() {
369                 model.decrementBusyCounter();
370             }
371             @Override
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
377             }
378             @Override
379             public void onNoRows() {
380                 if (TextUtils.isEmpty(accFilter))
381                     return;
382
383                 debug("description", "Trying transaction search without preferred account filter");
384
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";
388                 params.remove(1);
389                 debug("description", broaderSql);
390                 debug("description", description);
391
392                 runOnUiThread(() -> Snackbar.make(b.newTransactionNav,
393                         R.string.ignoring_preferred_account, Snackbar.LENGTH_INDEFINITE)
394                                             .show());
395
396                 MLDB.queryInBackground(broaderSql, new String[]{description},
397                         new MLDB.CallbackHelper() {
398                             @Override
399                             public void onStart() {
400                                 model.incrementBusyCounter();
401                             }
402                             @Override
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,
407                                         transactionId));
408                                 return false;
409                             }
410                             @Override
411                             public void onDone() {
412                                 model.decrementBusyCounter();
413                             }
414                         });
415             }
416         });
417     }
418     private void onFabPressed() {
419         b.fabAdd.hide();
420         Misc.hideSoftKeyboard(this);
421
422         LedgerTransaction tr = model.constructLedgerTransaction();
423
424         onTransactionSave(tr);
425     }
426     @Override
427     public Context getContext() {
428         return this;
429     }
430     @Override
431     public void showManagedFab() {
432         if (Objects.requireNonNull(model.isSubmittable()
433                                         .getValue()))
434             fabManager.showFab();
435     }
436     @Override
437     public void hideManagedFab() {
438         fabManager.hideFab();
439     }
440 }