]> git.ktnx.net Git - mobile-ledger.git/blob - app/src/main/java/net/ktnx/mobileledger/ui/activity/MainActivity.java
c3331ce6abde448a22d9d740bf650431f411f929
[mobile-ledger.git] / app / src / main / java / net / ktnx / mobileledger / ui / activity / MainActivity.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.activity;
19
20 import android.content.Context;
21 import android.content.Intent;
22 import android.content.pm.PackageInfo;
23 import android.content.pm.ShortcutInfo;
24 import android.content.pm.ShortcutManager;
25 import android.content.res.ColorStateList;
26 import android.graphics.Color;
27 import android.graphics.drawable.Icon;
28 import android.os.Build;
29 import android.os.Bundle;
30 import android.text.format.DateUtils;
31 import android.util.Log;
32 import android.view.View;
33 import android.view.animation.AnimationUtils;
34 import android.widget.TextView;
35
36 import androidx.annotation.NonNull;
37 import androidx.annotation.Nullable;
38 import androidx.appcompat.app.ActionBarDrawerToggle;
39 import androidx.appcompat.app.AlertDialog;
40 import androidx.core.view.GravityCompat;
41 import androidx.drawerlayout.widget.DrawerLayout;
42 import androidx.fragment.app.Fragment;
43 import androidx.fragment.app.FragmentActivity;
44 import androidx.lifecycle.LiveData;
45 import androidx.lifecycle.MutableLiveData;
46 import androidx.lifecycle.ViewModelProvider;
47 import androidx.recyclerview.widget.LinearLayoutManager;
48 import androidx.recyclerview.widget.RecyclerView;
49 import androidx.viewpager2.adapter.FragmentStateAdapter;
50 import androidx.viewpager2.widget.ViewPager2;
51
52 import com.google.android.material.snackbar.Snackbar;
53
54 import net.ktnx.mobileledger.BackupsActivity;
55 import net.ktnx.mobileledger.R;
56 import net.ktnx.mobileledger.async.RetrieveTransactionsTask;
57 import net.ktnx.mobileledger.async.TransactionAccumulator;
58 import net.ktnx.mobileledger.databinding.ActivityMainBinding;
59 import net.ktnx.mobileledger.db.DB;
60 import net.ktnx.mobileledger.db.Option;
61 import net.ktnx.mobileledger.db.Profile;
62 import net.ktnx.mobileledger.db.TransactionWithAccounts;
63 import net.ktnx.mobileledger.model.Data;
64 import net.ktnx.mobileledger.model.LedgerTransaction;
65 import net.ktnx.mobileledger.ui.FabManager;
66 import net.ktnx.mobileledger.ui.MainModel;
67 import net.ktnx.mobileledger.ui.account_summary.AccountSummaryFragment;
68 import net.ktnx.mobileledger.ui.new_transaction.NewTransactionActivity;
69 import net.ktnx.mobileledger.ui.profiles.ProfileDetailActivity;
70 import net.ktnx.mobileledger.ui.profiles.ProfilesRecyclerViewAdapter;
71 import net.ktnx.mobileledger.ui.templates.TemplatesActivity;
72 import net.ktnx.mobileledger.ui.transaction_list.TransactionListFragment;
73 import net.ktnx.mobileledger.utils.Colors;
74 import net.ktnx.mobileledger.utils.Logger;
75 import net.ktnx.mobileledger.utils.Misc;
76
77 import org.jetbrains.annotations.NotNull;
78
79 import java.util.ArrayList;
80 import java.util.Date;
81 import java.util.List;
82 import java.util.Locale;
83 import java.util.Objects;
84
85 /*
86  * TODO: reports
87  *  */
88
89 public class MainActivity extends ProfileThemedActivity implements FabManager.FabHandler {
90     public static final String TAG = "main-act";
91     public static final String STATE_CURRENT_PAGE = "current_page";
92     public static final String BUNDLE_SAVED_STATE = "bundle_savedState";
93     public static final String STATE_ACC_FILTER = "account_filter";
94     private static final boolean FAB_HIDDEN = false;
95     private static final boolean FAB_SHOWN = true;
96     private ConverterThread converterThread = null;
97     private SectionsPagerAdapter mSectionsPagerAdapter;
98     private ProfilesRecyclerViewAdapter mProfileListAdapter;
99     private int mCurrentPage;
100     private boolean mBackMeansToAccountList = false;
101     private DrawerLayout.SimpleDrawerListener drawerListener;
102     private ActionBarDrawerToggle barDrawerToggle;
103     private ViewPager2.OnPageChangeCallback pageChangeCallback;
104     private Profile profile;
105     private MainModel mainModel;
106     private ActivityMainBinding b;
107     private int fabVerticalOffset;
108     private FabManager fabManager;
109     @Override
110     protected void onStart() {
111         super.onStart();
112
113         Logger.debug(TAG, "onStart()");
114
115         b.mainPager.setCurrentItem(mCurrentPage, false);
116     }
117     @Override
118     protected void onSaveInstanceState(@NotNull Bundle outState) {
119         super.onSaveInstanceState(outState);
120         outState.putInt(STATE_CURRENT_PAGE, b.mainPager.getCurrentItem());
121         if (mainModel.getAccountFilter()
122                      .getValue() != null)
123             outState.putString(STATE_ACC_FILTER, mainModel.getAccountFilter()
124                                                           .getValue());
125     }
126     @Override
127     protected void onDestroy() {
128         mSectionsPagerAdapter = null;
129         b.navProfileList.setAdapter(null);
130         b.drawerLayout.removeDrawerListener(drawerListener);
131         drawerListener = null;
132         b.drawerLayout.removeDrawerListener(barDrawerToggle);
133         barDrawerToggle = null;
134         b.mainPager.unregisterOnPageChangeCallback(pageChangeCallback);
135         pageChangeCallback = null;
136         super.onDestroy();
137     }
138     @Override
139     protected void onResume() {
140         super.onResume();
141         fabShouldShow();
142     }
143     @Override
144     protected void onCreate(Bundle savedInstanceState) {
145         Logger.debug(TAG, "onCreate()/entry");
146         super.onCreate(savedInstanceState);
147         Logger.debug(TAG, "onCreate()/after super");
148         b = ActivityMainBinding.inflate(getLayoutInflater());
149         setContentView(b.getRoot());
150
151         mainModel = new ViewModelProvider(this).get(MainModel.class);
152
153         mSectionsPagerAdapter = new SectionsPagerAdapter(this);
154
155         Bundle extra = getIntent().getBundleExtra(BUNDLE_SAVED_STATE);
156         if (extra != null && savedInstanceState == null)
157             savedInstanceState = extra;
158
159
160         setSupportActionBar(b.toolbar);
161
162         Data.observeProfile(this, this::onProfileChanged);
163
164         Data.profiles.observe(this, this::onProfileListChanged);
165
166         Data.backgroundTaskProgress.observe(this, this::onRetrieveProgress);
167         Data.backgroundTasksRunning.observe(this, this::onRetrieveRunningChanged);
168
169         if (barDrawerToggle == null) {
170             barDrawerToggle = new ActionBarDrawerToggle(this, b.drawerLayout, b.toolbar, R.string.navigation_drawer_open, R.string.navigation_drawer_close);
171             b.drawerLayout.addDrawerListener(barDrawerToggle);
172         }
173         barDrawerToggle.syncState();
174
175         try {
176             PackageInfo pi = getApplicationContext().getPackageManager()
177                                                     .getPackageInfo(getPackageName(), 0);
178             ((TextView) b.navUpper.findViewById(R.id.drawer_version_text)).setText(pi.versionName);
179             ((TextView) b.noProfilesLayout.findViewById(R.id.drawer_version_text)).setText(pi.versionName);
180         }
181         catch (Exception e) {
182             e.printStackTrace();
183         }
184
185         markDrawerItemCurrent(R.id.nav_account_summary);
186
187         b.mainPager.setAdapter(mSectionsPagerAdapter);
188         b.mainPager.setOffscreenPageLimit(1);
189
190         if (pageChangeCallback == null) {
191             pageChangeCallback = new ViewPager2.OnPageChangeCallback() {
192                 @Override
193                 public void onPageSelected(int position) {
194                     mCurrentPage = position;
195                     switch (position) {
196                         case 0:
197                             markDrawerItemCurrent(R.id.nav_account_summary);
198                             break;
199                         case 1:
200                             markDrawerItemCurrent(R.id.nav_latest_transactions);
201                             break;
202                         default:
203                             Log.e(TAG, String.format("Unexpected page index %d", position));
204                     }
205
206                     super.onPageSelected(position);
207                 }
208             };
209             b.mainPager.registerOnPageChangeCallback(pageChangeCallback);
210         }
211
212         mCurrentPage = 0;
213         if (savedInstanceState != null) {
214             int currentPage = savedInstanceState.getInt(STATE_CURRENT_PAGE, -1);
215             if (currentPage != -1) {
216                 mCurrentPage = currentPage;
217             }
218             mainModel.getAccountFilter()
219                      .setValue(savedInstanceState.getString(STATE_ACC_FILTER, null));
220         }
221
222         b.btnNoProfilesAdd.setOnClickListener(v -> ProfileDetailActivity.start(this, null));
223
224         b.btnAddTransaction.setOnClickListener(this::fabNewTransactionClicked);
225
226         b.navNewProfileButton.setOnClickListener(v -> ProfileDetailActivity.start(this, null));
227
228         b.transactionListCancelDownload.setOnClickListener(this::onStopTransactionRefreshClick);
229
230         if (mProfileListAdapter == null)
231             mProfileListAdapter = new ProfilesRecyclerViewAdapter();
232         b.navProfileList.setAdapter(mProfileListAdapter);
233
234         mProfileListAdapter.editingProfiles.observe(this, newValue -> {
235             if (newValue) {
236                 b.navProfilesStartEdit.setVisibility(View.GONE);
237                 b.navProfilesCancelEdit.setVisibility(View.VISIBLE);
238                 b.navNewProfileButton.setVisibility(View.VISIBLE);
239                 if (b.drawerLayout.isDrawerOpen(GravityCompat.START)) {
240                     b.navProfilesStartEdit.startAnimation(
241                             AnimationUtils.loadAnimation(MainActivity.this, R.anim.fade_out));
242                     b.navProfilesCancelEdit.startAnimation(
243                             AnimationUtils.loadAnimation(MainActivity.this, R.anim.fade_in));
244                     b.navNewProfileButton.startAnimation(
245                             AnimationUtils.loadAnimation(MainActivity.this, R.anim.fade_in));
246                 }
247             }
248             else {
249                 b.navProfilesCancelEdit.setVisibility(View.GONE);
250                 b.navProfilesStartEdit.setVisibility(View.VISIBLE);
251                 b.navNewProfileButton.setVisibility(View.GONE);
252                 if (b.drawerLayout.isDrawerOpen(GravityCompat.START)) {
253                     b.navProfilesCancelEdit.startAnimation(
254                             AnimationUtils.loadAnimation(MainActivity.this, R.anim.fade_out));
255                     b.navProfilesStartEdit.startAnimation(
256                             AnimationUtils.loadAnimation(MainActivity.this, R.anim.fade_in));
257                     b.navNewProfileButton.startAnimation(
258                             AnimationUtils.loadAnimation(MainActivity.this, R.anim.fade_out));
259                 }
260             }
261
262             mProfileListAdapter.notifyDataSetChanged();
263         });
264
265         fabManager = new FabManager(b.btnAddTransaction);
266
267         LinearLayoutManager llm = new LinearLayoutManager(this);
268
269         llm.setOrientation(RecyclerView.VERTICAL);
270         b.navProfileList.setLayoutManager(llm);
271
272         b.navProfilesStartEdit.setOnClickListener((v) -> mProfileListAdapter.flipEditingProfiles());
273         b.navProfilesCancelEdit.setOnClickListener((v) -> mProfileListAdapter.flipEditingProfiles());
274         b.navProfileListHeadButtons.setOnClickListener((v) -> mProfileListAdapter.flipEditingProfiles());
275         if (drawerListener == null) {
276             drawerListener = new DrawerLayout.SimpleDrawerListener() {
277                 @Override
278                 public void onDrawerSlide(@NonNull View drawerView, float slideOffset) {
279                     if (slideOffset > 0.2)
280                         fabManager.hideFab();
281                 }
282                 @Override
283                 public void onDrawerClosed(View drawerView) {
284                     super.onDrawerClosed(drawerView);
285                     mProfileListAdapter.setAnimationsEnabled(false);
286                     mProfileListAdapter.editingProfiles.setValue(false);
287                     Data.drawerOpen.setValue(false);
288                     fabShouldShow();
289                 }
290                 @Override
291                 public void onDrawerOpened(View drawerView) {
292                     super.onDrawerOpened(drawerView);
293                     mProfileListAdapter.setAnimationsEnabled(true);
294                     Data.drawerOpen.setValue(true);
295                     fabManager.hideFab();
296                 }
297             };
298             b.drawerLayout.addDrawerListener(drawerListener);
299         }
300
301         Data.drawerOpen.observe(this, open -> {
302             if (open)
303                 b.drawerLayout.open();
304             else
305                 b.drawerLayout.close();
306         });
307
308         mainModel.getUpdateError()
309                  .observe(this, (error) -> {
310                      if (error == null)
311                          return;
312
313                      Snackbar.make(b.mainPager, error, Snackbar.LENGTH_INDEFINITE)
314                              .show();
315                      mainModel.clearUpdateError();
316                  });
317         Data.locale.observe(this, l -> refreshLastUpdateInfo());
318         Data.lastUpdateDate.observe(this, date -> refreshLastUpdateInfo());
319         Data.lastUpdateTransactionCount.observe(this, date -> refreshLastUpdateInfo());
320         Data.lastUpdateAccountCount.observe(this, date -> refreshLastUpdateInfo());
321         b.navAccountSummary.setOnClickListener(this::onAccountSummaryClicked);
322         b.navLatestTransactions.setOnClickListener(this::onLatestTransactionsClicked);
323         b.navPatterns.setOnClickListener(this::onPatternsClick);
324         b.navBackupRestore.setOnClickListener(this::onBackupRestoreClick);
325     }
326     private void onBackupRestoreClick(View view) {
327         Intent intent = new Intent(this, BackupsActivity.class);
328         startActivity(intent);
329     }
330     private void onPatternsClick(View view) {
331         Intent intent = new Intent(this, TemplatesActivity.class);
332         startActivity(intent);
333     }
334     private void scheduleDataRetrievalIfStale(long lastUpdate) {
335         long now = new Date().getTime();
336         if ((lastUpdate == 0) || (now > (lastUpdate + (24 * 3600 * 1000)))) {
337             if (lastUpdate == 0)
338                 Logger.debug("db::", "WEB data never fetched. scheduling a fetch");
339             else
340                 Logger.debug("db", String.format(Locale.ENGLISH,
341                         "WEB data last fetched at %1.3f and now is %1.3f. re-fetching",
342                         lastUpdate / 1000f, now / 1000f));
343
344             mainModel.scheduleTransactionListRetrieval();
345         }
346     }
347     private void createShortcuts(List<Profile> list) {
348         if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N_MR1)
349             return;
350
351         ShortcutManager sm = getSystemService(ShortcutManager.class);
352         List<ShortcutInfo> shortcuts = new ArrayList<>();
353         int i = 0;
354         for (Profile p : list) {
355             if (shortcuts.size() >= sm.getMaxShortcutCountPerActivity())
356                 break;
357
358             if (!p.permitPosting())
359                 continue;
360
361             final ShortcutInfo.Builder builder =
362                     new ShortcutInfo.Builder(this, "new_transaction_" + p.getId());
363             ShortcutInfo si = builder.setShortLabel(p.getName())
364                                      .setIcon(Icon.createWithResource(this,
365                                              R.drawable.thick_plus_icon))
366                                      .setIntent(new Intent(Intent.ACTION_VIEW, null, this,
367                                              NewTransactionActivity.class).putExtra(
368                                              ProfileThemedActivity.PARAM_PROFILE_ID, p.getId())
369                                                                           .putExtra(
370                                                                                   ProfileThemedActivity.PARAM_THEME,
371                                                                                   p.getTheme()))
372                                      .setRank(i)
373                                      .build();
374             shortcuts.add(si);
375             i++;
376         }
377         sm.setDynamicShortcuts(shortcuts);
378     }
379     private void onProfileListChanged(List<Profile> newList) {
380         if ((newList == null) || newList.isEmpty()) {
381             b.noProfilesLayout.setVisibility(View.VISIBLE);
382             b.mainAppLayout.setVisibility(View.GONE);
383             return;
384         }
385
386         b.mainAppLayout.setVisibility(View.VISIBLE);
387         b.noProfilesLayout.setVisibility(View.GONE);
388
389         b.navProfileList.setMinimumHeight(
390                 (int) (getResources().getDimension(R.dimen.thumb_row_height) * newList.size()));
391
392         Logger.debug("profiles", "profile list changed");
393         mProfileListAdapter.setProfileList(newList);
394
395         createShortcuts(newList);
396
397         final Profile currentProfile = Data.getProfile();
398         Profile replacementProfile = null;
399         if (currentProfile != null) {
400             for (Profile p : newList) {
401                 if (p.getId() == currentProfile.getId()) {
402                     replacementProfile = p;
403                     break;
404                 }
405             }
406         }
407
408         if (replacementProfile == null) {
409             Logger.debug(TAG, "Switching profile because the current is no longer available");
410             Data.setCurrentProfile(newList.get(0));
411         }
412         else {
413             Data.setCurrentProfile(replacementProfile);
414         }
415     }
416     /**
417      * called when the current profile has changed
418      */
419     private void onProfileChanged(@Nullable Profile newProfile) {
420         if (this.profile != null) {
421             if (this.profile.equals(newProfile))
422                 return;
423         }
424
425         boolean haveProfile = newProfile != null;
426
427         if (haveProfile)
428             setTitle(newProfile.getName());
429         else
430             setTitle(R.string.app_name);
431
432         int newProfileTheme = haveProfile ? newProfile.getTheme() : Colors.DEFAULT_HUE_DEG;
433         if (newProfileTheme != Colors.profileThemeId) {
434             Logger.debug("profiles",
435                     String.format(Locale.ENGLISH, "profile theme %d → %d", Colors.profileThemeId,
436                             newProfileTheme));
437             Colors.profileThemeId = newProfileTheme;
438             profileThemeChanged();
439             // profileThemeChanged would restart the activity, so no need to reload the
440             // data sets below
441             return;
442         }
443
444         final boolean sameProfileId = (newProfile != null) && (this.profile != null) &&
445                                       this.profile.getId() == newProfile.getId();
446
447         this.profile = newProfile;
448
449         b.noProfilesLayout.setVisibility(haveProfile ? View.GONE : View.VISIBLE);
450         b.pagerLayout.setVisibility(haveProfile ? View.VISIBLE : View.VISIBLE);
451
452         mProfileListAdapter.notifyDataSetChanged();
453
454         if (haveProfile) {
455             if (newProfile.permitPosting()) {
456                 b.toolbar.setSubtitle(null);
457                 b.btnAddTransaction.show();
458             }
459             else {
460                 b.toolbar.setSubtitle(R.string.profile_subtitle_read_only);
461                 b.btnAddTransaction.hide();
462             }
463         }
464         else {
465             b.toolbar.setSubtitle(null);
466             b.btnAddTransaction.hide();
467         }
468
469         updateLastUpdateTextFromDB();
470
471         if (sameProfileId) {
472             Logger.debug(TAG, String.format(Locale.ROOT, "Short-cut profile 'changed' to %d",
473                     newProfile.getId()));
474             return;
475         }
476
477         mainModel.getAccountFilter()
478                  .observe(this, this::onAccountFilterChanged);
479
480         mainModel.stopTransactionsRetrieval();
481         mainModel.clearTransactions();
482     }
483     private void onAccountFilterChanged(String accFilter) {
484         Logger.debug(TAG, "account filter changed, reloading transactions");
485 //                     mainModel.scheduleTransactionListReload();
486         LiveData<List<TransactionWithAccounts>> transactions =
487                 new MutableLiveData<>(new ArrayList<>());
488         if (profile != null) {
489             if (accFilter == null || accFilter.isEmpty()) {
490                 transactions = DB.get()
491                                  .getTransactionDAO()
492                                  .getAllWithAccounts(profile.getId());
493             }
494             else {
495                 transactions = DB.get()
496                                  .getTransactionDAO()
497                                  .getAllWithAccountsFiltered(profile.getId(), accFilter);
498             }
499         }
500
501         transactions.observe(this, list -> {
502             Logger.debug(TAG,
503                     String.format(Locale.ROOT, "got transaction list from DB (%d transactions)",
504                             list.size()));
505
506             if (converterThread != null)
507                 converterThread.interrupt();
508             converterThread = new ConverterThread(mainModel, list, accFilter);
509             converterThread.start();
510         });
511     }
512     private void profileThemeChanged() {
513         // un-hook all observed LiveData
514         Data.removeProfileObservers(this);
515         Data.profiles.removeObservers(this);
516         Data.lastUpdateTransactionCount.removeObservers(this);
517         Data.lastUpdateAccountCount.removeObservers(this);
518         Data.lastUpdateDate.removeObservers(this);
519
520         Logger.debug(TAG, "profileThemeChanged(): recreating activity");
521         recreate();
522     }
523     public void fabNewTransactionClicked(View view) {
524         Intent intent = new Intent(this, NewTransactionActivity.class);
525         intent.putExtra(ProfileThemedActivity.PARAM_PROFILE_ID, profile.getId());
526         intent.putExtra(ProfileThemedActivity.PARAM_THEME, profile.getTheme());
527         startActivity(intent);
528         overridePendingTransition(R.anim.slide_in_up, R.anim.dummy);
529     }
530     public void markDrawerItemCurrent(int id) {
531         TextView item = b.drawerLayout.findViewById(id);
532         item.setBackgroundColor(Colors.tableRowDarkBG);
533
534         for (int i = 0; i < b.navActions.getChildCount(); i++) {
535             View view = b.navActions.getChildAt(i);
536             if (view.getId() != id) {
537                 view.setBackgroundColor(Color.TRANSPARENT);
538             }
539         }
540     }
541     public void onAccountSummaryClicked(View view) {
542         b.drawerLayout.closeDrawers();
543
544         showAccountSummaryFragment();
545     }
546     private void showAccountSummaryFragment() {
547         b.mainPager.setCurrentItem(0, true);
548         mainModel.getAccountFilter()
549                  .setValue(null);
550     }
551     public void onLatestTransactionsClicked(View view) {
552         b.drawerLayout.closeDrawers();
553
554         showTransactionsFragment(null);
555     }
556     public void showTransactionsFragment(String accName) {
557         mainModel.getAccountFilter()
558                  .setValue(accName);
559         b.mainPager.setCurrentItem(1, true);
560     }
561     public void showAccountTransactions(String accountName) {
562         mBackMeansToAccountList = true;
563         showTransactionsFragment(accountName);
564     }
565     @Override
566     public void onBackPressed() {
567         if (b.drawerLayout.isDrawerOpen(GravityCompat.START)) {
568             b.drawerLayout.closeDrawer(GravityCompat.START);
569         }
570         else {
571             if (mBackMeansToAccountList && (b.mainPager.getCurrentItem() == 1)) {
572                 mainModel.getAccountFilter()
573                          .setValue(null);
574                 showAccountSummaryFragment();
575                 mBackMeansToAccountList = false;
576             }
577             else {
578                 Logger.debug(TAG, String.format(Locale.ENGLISH, "manager stack: %d",
579                         getSupportFragmentManager().getBackStackEntryCount()));
580
581                 super.onBackPressed();
582             }
583         }
584     }
585     public void updateLastUpdateTextFromDB() {
586         if (profile == null)
587             return;
588
589         DB.get()
590           .getOptionDAO()
591           .load(profile.getId(), Option.OPT_LAST_SCRAPE)
592           .observe(this, opt -> {
593               long lastUpdate = 0;
594               if (opt != null) {
595                   try {
596                       lastUpdate = Long.parseLong(opt.getValue());
597                   }
598                   catch (NumberFormatException ex) {
599                       Logger.debug(TAG, String.format("Error parsing '%s' as long", opt.getValue()),
600                               ex);
601                   }
602               }
603
604               if (lastUpdate == 0) {
605                   Data.lastUpdateDate.postValue(null);
606               }
607               else {
608                   Data.lastUpdateDate.postValue(new Date(lastUpdate));
609               }
610
611               scheduleDataRetrievalIfStale(lastUpdate);
612           });
613     }
614     private void refreshLastUpdateInfo() {
615         final int formatFlags = DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_SHOW_YEAR |
616                                 DateUtils.FORMAT_SHOW_TIME | DateUtils.FORMAT_NUMERIC_DATE;
617         String templateForTransactions =
618                 getResources().getString(R.string.transaction_count_summary);
619         String templateForAccounts = getResources().getString(R.string.account_count_summary);
620         Integer accountCount = Data.lastUpdateAccountCount.getValue();
621         Integer transactionCount = Data.lastUpdateTransactionCount.getValue();
622         Date lastUpdate = Data.lastUpdateDate.getValue();
623         if (lastUpdate == null) {
624             Data.lastTransactionsUpdateText.setValue("----");
625             Data.lastAccountsUpdateText.setValue("----");
626         }
627         else {
628             Data.lastTransactionsUpdateText.setValue(
629                     String.format(Objects.requireNonNull(Data.locale.getValue()),
630                             templateForTransactions,
631                             transactionCount == null ? 0 : transactionCount,
632                             DateUtils.formatDateTime(this, lastUpdate.getTime(), formatFlags)));
633             Data.lastAccountsUpdateText.setValue(
634                     String.format(Objects.requireNonNull(Data.locale.getValue()),
635                             templateForAccounts, accountCount == null ? 0 : accountCount,
636                             DateUtils.formatDateTime(this, lastUpdate.getTime(), formatFlags)));
637         }
638     }
639     public void onStopTransactionRefreshClick(View view) {
640         Logger.debug(TAG, "Cancelling transactions refresh");
641         mainModel.stopTransactionsRetrieval();
642         b.transactionListCancelDownload.setEnabled(false);
643     }
644     public void onRetrieveRunningChanged(Boolean running) {
645         if (running) {
646             b.transactionListCancelDownload.setEnabled(true);
647             ColorStateList csl = Colors.getColorStateList();
648             b.transactionListProgressBar.setIndeterminateTintList(csl);
649             b.transactionListProgressBar.setProgressTintList(csl);
650             b.transactionListProgressBar.setIndeterminate(true);
651             if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
652                 b.transactionListProgressBar.setProgress(0, false);
653             }
654             else {
655                 b.transactionListProgressBar.setProgress(0);
656             }
657             b.transactionProgressLayout.setVisibility(View.VISIBLE);
658         }
659         else {
660             b.transactionProgressLayout.setVisibility(View.GONE);
661         }
662     }
663     public void onRetrieveProgress(@Nullable RetrieveTransactionsTask.Progress progress) {
664         if (progress == null ||
665             progress.getState() == RetrieveTransactionsTask.ProgressState.FINISHED)
666         {
667             Logger.debug(TAG, "progress: Done");
668             b.transactionProgressLayout.setVisibility(View.GONE);
669
670             mainModel.transactionRetrievalDone();
671
672             String error = (progress == null) ? null : progress.getError();
673             if (error != null) {
674                 if (error.equals(RetrieveTransactionsTask.Result.ERR_JSON_PARSER_ERROR))
675                     error = getResources().getString(R.string.err_json_parser_error);
676
677                 AlertDialog.Builder builder = new AlertDialog.Builder(this);
678                 builder.setMessage(error);
679                 builder.setPositiveButton(R.string.btn_profile_options, (dialog, which) -> {
680                     Logger.debug(TAG, "will start profile editor");
681                     ProfileDetailActivity.start(this, profile);
682                 });
683                 builder.create()
684                        .show();
685                 return;
686             }
687
688             return;
689         }
690
691
692         b.transactionListCancelDownload.setEnabled(true);
693 //        ColorStateList csl = Colors.getColorStateList();
694 //        progressBar.setIndeterminateTintList(csl);
695 //        progressBar.setProgressTintList(csl);
696 //        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N)
697 //            progressBar.setProgress(0, false);
698 //        else
699 //            progressBar.setProgress(0);
700         b.transactionProgressLayout.setVisibility(View.VISIBLE);
701
702         if (progress.isIndeterminate() || (progress.getTotal() <= 0)) {
703             b.transactionListProgressBar.setIndeterminate(true);
704             Logger.debug(TAG, "progress: indeterminate");
705         }
706         else {
707             if (b.transactionListProgressBar.isIndeterminate()) {
708                 b.transactionListProgressBar.setIndeterminate(false);
709             }
710 //            Logger.debug(TAG,
711 //                    String.format(Locale.US, "progress: %d/%d", progress.getProgress(),
712 //                    progress.getTotal
713 //                    ()));
714             b.transactionListProgressBar.setMax(progress.getTotal());
715             // for some reason animation doesn't work - no progress is shown (stick at 0)
716             // on lineageOS 14.1 (Nougat, 7.1.2)
717             if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N)
718                 b.transactionListProgressBar.setProgress(progress.getProgress(), false);
719             else
720                 b.transactionListProgressBar.setProgress(progress.getProgress());
721         }
722     }
723     public void fabShouldShow() {
724         if ((profile != null) && profile.permitPosting() && !b.drawerLayout.isOpen())
725             fabManager.showFab();
726     }
727     @Override
728     public Context getContext() {
729         return this;
730     }
731     @Override
732     public void showManagedFab() {
733         fabShouldShow();
734     }
735     @Override
736     public void hideManagedFab() {
737         fabManager.hideFab();
738     }
739     public static class SectionsPagerAdapter extends FragmentStateAdapter {
740
741         public SectionsPagerAdapter(@NonNull FragmentActivity fragmentActivity) {
742             super(fragmentActivity);
743         }
744         @NotNull
745         @Override
746         public Fragment createFragment(int position) {
747             Logger.debug(TAG, String.format(Locale.ENGLISH, "Switching to fragment %d", position));
748             switch (position) {
749                 case 0:
750 //                    debug(TAG, "Creating account summary fragment");
751                     return new AccountSummaryFragment();
752                 case 1:
753                     return new TransactionListFragment();
754                 default:
755                     throw new IllegalStateException(
756                             String.format("Unexpected fragment index: " + "%d", position));
757             }
758         }
759
760         @Override
761         public int getItemCount() {
762             return 2;
763         }
764     }
765
766     static private class ConverterThread extends Thread {
767         private final List<TransactionWithAccounts> list;
768         private final MainModel model;
769         private final String accFilter;
770         public ConverterThread(@NonNull MainModel model,
771                                @NonNull List<TransactionWithAccounts> list, String accFilter) {
772             this.model = model;
773             this.list = list;
774             this.accFilter = accFilter;
775         }
776         @Override
777         public void run() {
778             TransactionAccumulator accumulator = new TransactionAccumulator(accFilter, accFilter);
779
780             for (TransactionWithAccounts tr : list) {
781                 if (isInterrupted()) {
782                     Logger.debug(TAG, "ConverterThread bailing out on interrupt");
783                     return;
784                 }
785                 accumulator.put(new LedgerTransaction(tr));
786             }
787
788             if (isInterrupted()) {
789                 Logger.debug(TAG, "ConverterThread bailing out on interrupt");
790                 return;
791             }
792
793             Logger.debug(TAG, "ConverterThread publishing results");
794
795             Misc.onMainThread(() -> accumulator.publishResults(model));
796         }
797     }
798 }