]> git.ktnx.net Git - mobile-ledger.git/blob - app/src/main/java/net/ktnx/mobileledger/ui/new_transaction/NewTransactionActivity.java
move async DB stuff away of AsyncTask
[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.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;
29
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;
36
37 import com.google.android.material.dialog.MaterialAlertDialogBuilder;
38
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;
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     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;
80     @Override
81     protected void onCreate(Bundle savedInstanceState) {
82         super.onCreate(savedInstanceState);
83
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);
93                 finish();
94             }
95             else
96                 b.toolbar.setSubtitle(profile.getName());
97         });
98
99         NavHostFragment navHostFragment = (NavHostFragment) Objects.requireNonNull(
100                 getSupportFragmentManager().findFragmentById(R.id.new_transaction_nav));
101         navController = navHostFragment.getNavController();
102
103         Objects.requireNonNull(getSupportActionBar())
104                .setDisplayHomeAsUpEnabled(true);
105
106         model = new ViewModelProvider(this).get(NewTransactionModel.class);
107
108         qrScanLauncher = QR.registerLauncher(this, this);
109
110         fabManager = new FabManager(b.fabAdd);
111
112         model.isSubmittable()
113              .observe(this, isSubmittable -> {
114                  if (isSubmittable) {
115                      fabManager.showFab();
116                  }
117                  else {
118                      fabManager.hideFab();
119                  }
120              });
121 //        viewModel.checkTransactionSubmittable(listAdapter);
122
123         b.fabAdd.setOnClickListener(v -> onFabPressed());
124     }
125     @Override
126     protected void initProfile() {
127         long profileId = getIntent().getLongExtra(PARAM_PROFILE_ID, 0);
128         int profileHue = getIntent().getIntExtra(PARAM_THEME, -1);
129
130         if (profileHue < 0) {
131             Logger.debug(TAG, "Started with invalid/missing theme; quitting");
132             finish();
133             return;
134         }
135
136         if (profileId <= 0) {
137             Logger.debug(TAG, "Started with invalid/missing profile_id; quitting");
138             finish();
139             return;
140         }
141
142         setupProfileColors(profileHue);
143         initProfile(profileId);
144     }
145     @Override
146     public void finish() {
147         super.finish();
148         overridePendingTransition(R.anim.dummy, R.anim.slide_out_down);
149     }
150     @Override
151     public boolean onOptionsItemSelected(MenuItem item) {
152         if (item.getItemId() == android.R.id.home) {
153             finish();
154             return true;
155         }
156         return super.onOptionsItemSelected(item);
157     }
158     public void onTransactionSave(LedgerTransaction tr) {
159         navController.navigate(R.id.action_newTransactionFragment_to_newTransactionSavingFragment);
160         try {
161
162             SendTransactionTask saver =
163                     new SendTransactionTask(this, mProfile, model.getSimulateSaveFlag());
164             saver.execute(tr);
165         }
166         catch (Exception e) {
167             debug("new-transaction", "Unknown error: " + e);
168
169             Bundle b = new Bundle();
170             b.putString("error", "unknown error");
171             navController.navigate(R.id.newTransactionFragment, b);
172         }
173     }
174     public boolean onSimulateCrashMenuItemClicked(MenuItem item) {
175         debug("crash", "Will crash intentionally");
176         GeneralBackgroundTasks.run(() -> { throw new RuntimeException("Simulated crash");});
177         return true;
178     }
179     public boolean onCreateOptionsMenu(Menu menu) {
180         super.onCreateOptionsMenu(menu);
181
182         if (!BuildConfig.DEBUG)
183             return true;
184
185         // Inflate the menu; this adds items to the action bar if it is present.
186         getMenuInflater().inflate(R.menu.new_transaction, menu);
187
188         MenuCompat.setGroupDividerEnabled(menu, true);
189
190         menu.findItem(R.id.action_simulate_save)
191             .setOnMenuItemClickListener(this::onToggleSimulateSaveMenuItemClicked);
192         menu.findItem(R.id.action_simulate_crash)
193             .setOnMenuItemClickListener(this::onSimulateCrashMenuItemClicked);
194
195         model.getSimulateSave()
196              .observe(this, state -> {
197                  menu.findItem(R.id.action_simulate_save)
198                      .setChecked(state);
199                  b.simulationLabel.setVisibility(state ? View.VISIBLE : View.GONE);
200              });
201
202         return true;
203     }
204
205
206     public int dp2px(float dp) {
207         return Math.round(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp,
208                 getResources().getDisplayMetrics()));
209     }
210     @Override
211     public void onTransactionSaveDone(String error, Object arg) {
212         Bundle b = new Bundle();
213         if (error != null) {
214             b.putString("error", error);
215             navController.navigate(R.id.action_newTransactionSavingFragment_Failure, b);
216         }
217         else {
218             navController.navigate(R.id.action_newTransactionSavingFragment_Success, b);
219
220             BaseDAO.runAsync(() -> commitToDb((LedgerTransaction) arg));
221         }
222     }
223     public void commitToDb(LedgerTransaction tr) {
224         TransactionWithAccounts dbTransaction = tr.toDBO();
225         DB.get()
226           .getTransactionDAO()
227           .appendSync(dbTransaction);
228     }
229     public boolean onToggleSimulateSaveMenuItemClicked(MenuItem item) {
230         model.toggleSimulateSave();
231         return true;
232     }
233
234     @Override
235     public void triggerQRScan() {
236         qrScanLauncher.launch(null);
237     }
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);
243     }
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))
250                .create()
251                .show();
252     }
253     public void onQRScanResult(String text) {
254         Logger.debug("qr", String.format("Got QR scan result [%s]", text));
255
256         if (Misc.emptyIsNull(text) == null)
257             return;
258
259         LiveData<List<TemplateHeader>> allTemplates = DB.get()
260                                                         .getTemplateDAO()
261                                                         .getTemplates();
262         allTemplates.observe(this, templateHeaders -> {
263             ArrayList<MatchedTemplate> matchingFallbackTemplates = new ArrayList<>();
264             ArrayList<MatchedTemplate> matchingTemplates = new ArrayList<>();
265
266             for (TemplateHeader ph : templateHeaders) {
267                 String patternSource = ph.getRegularExpression();
268                 if (Misc.emptyIsNull(patternSource) == null)
269                     continue;
270                 try {
271                     Pattern pattern = Pattern.compile(patternSource);
272                     Matcher matcher = pattern.matcher(text);
273                     if (!matcher.matches())
274                         continue;
275
276                     Logger.debug("pattern",
277                             String.format("Pattern '%s' [%s] matches '%s'", ph.getName(),
278                                     patternSource, text));
279                     if (ph.isFallback())
280                         matchingFallbackTemplates.add(
281                                 new MatchedTemplate(ph, matcher.toMatchResult()));
282                     else
283                         matchingTemplates.add(new MatchedTemplate(ph, matcher.toMatchResult()));
284                 }
285                 catch (ParcelFormatException e) {
286                     // ignored
287                     Logger.debug("pattern",
288                             String.format("Error compiling regular expression '%s'", patternSource),
289                             e);
290                 }
291             }
292
293             if (matchingTemplates.isEmpty())
294                 matchingTemplates = matchingFallbackTemplates;
295
296             if (matchingTemplates.isEmpty())
297                 alertNoTemplateMatch(text);
298             else if (matchingTemplates.size() == 1)
299                 model.applyTemplate(matchingTemplates.get(0), text);
300             else
301                 chooseTemplate(matchingTemplates, text);
302         });
303     }
304     private void chooseTemplate(ArrayList<MatchedTemplate> matchingTemplates, String matchedText) {
305         final String templateNameColumn = "name";
306         AbstractCursor cursor = new AbstractCursor() {
307             @Override
308             public int getCount() {
309                 return matchingTemplates.size();
310             }
311             @Override
312             public String[] getColumnNames() {
313                 return new String[]{"_id", templateNameColumn};
314             }
315             @Override
316             public String getString(int column) {
317                 if (column == 0)
318                     return String.valueOf(getPosition());
319                 return matchingTemplates.get(getPosition()).templateHead.getName();
320             }
321             @Override
322             public short getShort(int column) {
323                 if (column == 0)
324                     return (short) getPosition();
325                 return -1;
326             }
327             @Override
328             public int getInt(int column) {
329                 return getShort(column);
330             }
331             @Override
332             public long getLong(int column) {
333                 return getShort(column);
334             }
335             @Override
336             public float getFloat(int column) {
337                 return getShort(column);
338             }
339             @Override
340             public double getDouble(int column) {
341                 return getShort(column);
342             }
343             @Override
344             public boolean isNull(int column) {
345                 return false;
346             }
347             @Override
348             public int getColumnCount() {
349                 return 2;
350             }
351         };
352
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);
359                    dialog.dismiss();
360                })
361                .create()
362                .show();
363     }
364     public void onDescriptionSelected(String description) {
365         debug("description selected", description);
366         if (!model.accountListIsEmpty())
367             return;
368
369         BaseDAO.runAsync(() -> {
370             String accFilter = mProfile.getPreferredAccountsFilter();
371
372             TransactionDAO trDao = DB.get()
373                                      .getTransactionDAO();
374
375             TransactionWithAccounts tr = null;
376
377             if (Misc.emptyIsNull(accFilter) != null)
378                 tr = trDao.getFirstByDescriptionHavingAccountSync(description, accFilter);
379             if (tr == null)
380                 tr = trDao.getFirstByDescriptionSync(description);
381
382             if (tr != null)
383                 model.loadTransactionIntoModel(tr);
384         });
385     }
386     private void onFabPressed() {
387         fabManager.hideFab();
388         Misc.hideSoftKeyboard(this);
389
390         LedgerTransaction tr = model.constructLedgerTransaction();
391
392         onTransactionSave(tr);
393     }
394     @Override
395     public Context getContext() {
396         return this;
397     }
398     @Override
399     public void showManagedFab() {
400         if (Objects.requireNonNull(model.isSubmittable()
401                                         .getValue()))
402             fabManager.showFab();
403     }
404     @Override
405     public void hideManagedFab() {
406         fabManager.hideFab();
407     }
408 }