]> git.ktnx.net Git - mobile-ledger.git/blob - app/src/main/java/net/ktnx/mobileledger/ui/new_transaction/NewTransactionActivity.java
Room-based profile management
[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) {
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     public boolean onToggleSimulateSaveMenuItemClicked(MenuItem item) {
211         model.toggleSimulateSave();
212         return true;
213     }
214
215     @Override
216     public void triggerQRScan() {
217         qrScanLauncher.launch(null);
218     }
219     private void startNewPatternActivity(String scanned) {
220         Intent intent = new Intent(this, TemplatesActivity.class);
221         Bundle args = new Bundle();
222         args.putString(TemplatesActivity.ARG_ADD_TEMPLATE, scanned);
223         startActivity(intent, args);
224     }
225     private void alertNoTemplateMatch(String scanned) {
226         MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(this);
227         builder.setCancelable(true)
228                .setMessage(R.string.no_template_matches)
229                .setPositiveButton(R.string.add_button,
230                        (dialog, which) -> startNewPatternActivity(scanned))
231                .create()
232                .show();
233     }
234     public void onQRScanResult(String text) {
235         Logger.debug("qr", String.format("Got QR scan result [%s]", text));
236
237         if (Misc.emptyIsNull(text) == null)
238             return;
239
240         LiveData<List<TemplateHeader>> allTemplates = DB.get()
241                                                         .getTemplateDAO()
242                                                         .getTemplates();
243         allTemplates.observe(this, templateHeaders -> {
244             ArrayList<MatchedTemplate> matchingFallbackTemplates = new ArrayList<>();
245             ArrayList<MatchedTemplate> matchingTemplates = new ArrayList<>();
246
247             for (TemplateHeader ph : templateHeaders) {
248                 String patternSource = ph.getRegularExpression();
249                 if (Misc.emptyIsNull(patternSource) == null)
250                     continue;
251                 try {
252                     Pattern pattern = Pattern.compile(patternSource);
253                     Matcher matcher = pattern.matcher(text);
254                     if (!matcher.matches())
255                         continue;
256
257                     Logger.debug("pattern",
258                             String.format("Pattern '%s' [%s] matches '%s'", ph.getName(),
259                                     patternSource, text));
260                     if (ph.isFallback())
261                         matchingFallbackTemplates.add(
262                                 new MatchedTemplate(ph, matcher.toMatchResult()));
263                     else
264                         matchingTemplates.add(new MatchedTemplate(ph, matcher.toMatchResult()));
265                 }
266                 catch (ParcelFormatException e) {
267                     // ignored
268                     Logger.debug("pattern",
269                             String.format("Error compiling regular expression '%s'", patternSource),
270                             e);
271                 }
272             }
273
274             if (matchingTemplates.isEmpty())
275                 matchingTemplates = matchingFallbackTemplates;
276
277             if (matchingTemplates.isEmpty())
278                 alertNoTemplateMatch(text);
279             else if (matchingTemplates.size() == 1)
280                 model.applyTemplate(matchingTemplates.get(0), text);
281             else
282                 chooseTemplate(matchingTemplates, text);
283         });
284     }
285     private void chooseTemplate(ArrayList<MatchedTemplate> matchingTemplates, String matchedText) {
286         final String templateNameColumn = "name";
287         AbstractCursor cursor = new AbstractCursor() {
288             @Override
289             public int getCount() {
290                 return matchingTemplates.size();
291             }
292             @Override
293             public String[] getColumnNames() {
294                 return new String[]{"_id", templateNameColumn};
295             }
296             @Override
297             public String getString(int column) {
298                 if (column == 0)
299                     return String.valueOf(getPosition());
300                 return matchingTemplates.get(getPosition()).templateHead.getName();
301             }
302             @Override
303             public short getShort(int column) {
304                 if (column == 0)
305                     return (short) getPosition();
306                 return -1;
307             }
308             @Override
309             public int getInt(int column) {
310                 return getShort(column);
311             }
312             @Override
313             public long getLong(int column) {
314                 return getShort(column);
315             }
316             @Override
317             public float getFloat(int column) {
318                 return getShort(column);
319             }
320             @Override
321             public double getDouble(int column) {
322                 return getShort(column);
323             }
324             @Override
325             public boolean isNull(int column) {
326                 return false;
327             }
328             @Override
329             public int getColumnCount() {
330                 return 2;
331             }
332         };
333
334         MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(this);
335         builder.setCancelable(true)
336                .setTitle(R.string.choose_template_to_apply)
337                .setIcon(R.drawable.ic_baseline_auto_graph_24)
338                .setSingleChoiceItems(cursor, -1, templateNameColumn, (dialog, which) -> {
339                    model.applyTemplate(matchingTemplates.get(which), matchedText);
340                    dialog.dismiss();
341                })
342                .create()
343                .show();
344     }
345     public void descriptionSelected(String description) {
346         debug("description selected", description);
347         if (!model.accountListIsEmpty())
348             return;
349
350         AsyncTask.execute(() -> {
351             String accFilter = mProfile.getPreferredAccountsFilter();
352
353             TransactionDAO trDao = DB.get()
354                                      .getTransactionDAO();
355
356             TransactionWithAccounts tr;
357
358             if (Misc.emptyIsNull(accFilter) != null) {
359                 tr = trDao.getFirstByDescriptionHavingAccountSync(description, accFilter);
360                 if (tr != null) {
361                     model.loadTransactionIntoModel(tr);
362                     return;
363                 }
364             }
365
366             tr = trDao.getFirstByDescriptionSync(description);
367             if (tr != null)
368                 model.loadTransactionIntoModel(tr);
369         });
370     }
371     private void onFabPressed() {
372         fabManager.hideFab();
373         Misc.hideSoftKeyboard(this);
374
375         LedgerTransaction tr = model.constructLedgerTransaction();
376
377         onTransactionSave(tr);
378     }
379     @Override
380     public Context getContext() {
381         return this;
382     }
383     @Override
384     public void showManagedFab() {
385         if (Objects.requireNonNull(model.isSubmittable()
386                                         .getValue()))
387             fabManager.showFab();
388     }
389     @Override
390     public void hideManagedFab() {
391         fabManager.hideFab();
392     }
393 }