package net.ktnx.mobileledger.ui.activity;
+import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageInfo;
import android.content.pm.ShortcutInfo;
import android.widget.TextView;
import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
import androidx.appcompat.app.ActionBarDrawerToggle;
import androidx.appcompat.app.AlertDialog;
import androidx.core.view.GravityCompat;
import androidx.drawerlayout.widget.DrawerLayout;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentActivity;
+import androidx.lifecycle.LiveData;
+import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.ViewModelProvider;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import net.ktnx.mobileledger.R;
import net.ktnx.mobileledger.async.RetrieveTransactionsTask;
+import net.ktnx.mobileledger.async.TransactionAccumulator;
import net.ktnx.mobileledger.databinding.ActivityMainBinding;
+import net.ktnx.mobileledger.db.DB;
+import net.ktnx.mobileledger.db.Option;
+import net.ktnx.mobileledger.db.Profile;
+import net.ktnx.mobileledger.db.TransactionWithAccounts;
import net.ktnx.mobileledger.model.Data;
-import net.ktnx.mobileledger.model.MobileLedgerProfile;
+import net.ktnx.mobileledger.model.LedgerTransaction;
+import net.ktnx.mobileledger.ui.FabManager;
import net.ktnx.mobileledger.ui.MainModel;
import net.ktnx.mobileledger.ui.account_summary.AccountSummaryFragment;
import net.ktnx.mobileledger.ui.new_transaction.NewTransactionActivity;
+import net.ktnx.mobileledger.ui.profiles.ProfileDetailActivity;
import net.ktnx.mobileledger.ui.profiles.ProfilesRecyclerViewAdapter;
import net.ktnx.mobileledger.ui.templates.TemplatesActivity;
import net.ktnx.mobileledger.ui.transaction_list.TransactionListFragment;
import net.ktnx.mobileledger.utils.Colors;
import net.ktnx.mobileledger.utils.Logger;
-import net.ktnx.mobileledger.utils.MLDB;
+import net.ktnx.mobileledger.utils.Misc;
import org.jetbrains.annotations.NotNull;
* TODO: reports
* */
-public class MainActivity extends ProfileThemedActivity {
+public class MainActivity extends ProfileThemedActivity implements FabManager.FabHandler {
+ public static final String TAG = "main-act";
public static final String STATE_CURRENT_PAGE = "current_page";
public static final String BUNDLE_SAVED_STATE = "bundle_savedState";
public static final String STATE_ACC_FILTER = "account_filter";
+ private static final boolean FAB_HIDDEN = false;
+ private static final boolean FAB_SHOWN = true;
+ private ConverterThread converterThread = null;
private SectionsPagerAdapter mSectionsPagerAdapter;
private ProfilesRecyclerViewAdapter mProfileListAdapter;
private int mCurrentPage;
private DrawerLayout.SimpleDrawerListener drawerListener;
private ActionBarDrawerToggle barDrawerToggle;
private ViewPager2.OnPageChangeCallback pageChangeCallback;
- private MobileLedgerProfile profile;
+ private Profile profile;
private MainModel mainModel;
private ActivityMainBinding b;
+ private int fabVerticalOffset;
+ private FabManager fabManager;
@Override
protected void onStart() {
super.onStart();
- Logger.debug("MainActivity", "onStart()");
+ Logger.debug(TAG, "onStart()");
b.mainPager.setCurrentItem(mCurrentPage, false);
}
}
@Override
protected void onCreate(Bundle savedInstanceState) {
- Logger.debug("MainActivity", "onCreate()/entry");
+ Logger.debug(TAG, "onCreate()/entry");
super.onCreate(savedInstanceState);
- Logger.debug("MainActivity", "onCreate()/after super");
+ Logger.debug(TAG, "onCreate()/after super");
b = ActivityMainBinding.inflate(getLayoutInflater());
setContentView(b.getRoot());
Data.observeProfile(this, this::onProfileChanged);
Data.profiles.observe(this, this::onProfileListChanged);
+
Data.backgroundTaskProgress.observe(this, this::onRetrieveProgress);
Data.backgroundTasksRunning.observe(this, this::onRetrieveRunningChanged);
markDrawerItemCurrent(R.id.nav_account_summary);
b.mainPager.setAdapter(mSectionsPagerAdapter);
+ b.mainPager.setOffscreenPageLimit(1);
if (pageChangeCallback == null) {
pageChangeCallback = new ViewPager2.OnPageChangeCallback() {
markDrawerItemCurrent(R.id.nav_latest_transactions);
break;
default:
- Log.e("MainActivity",
- String.format("Unexpected page index %d", position));
+ Log.e(TAG, String.format("Unexpected page index %d", position));
}
super.onPageSelected(position);
.setValue(savedInstanceState.getString(STATE_ACC_FILTER, null));
}
- b.btnNoProfilesAdd.setOnClickListener(
- v -> MobileLedgerProfile.startEditProfileActivity(this, null));
+ b.btnNoProfilesAdd.setOnClickListener(v -> ProfileDetailActivity.start(this, null));
b.btnAddTransaction.setOnClickListener(this::fabNewTransactionClicked);
- b.navNewProfileButton.setOnClickListener(
- v -> MobileLedgerProfile.startEditProfileActivity(this, null));
+ b.navNewProfileButton.setOnClickListener(v -> ProfileDetailActivity.start(this, null));
b.transactionListCancelDownload.setOnClickListener(this::onStopTransactionRefreshClick);
mProfileListAdapter.notifyDataSetChanged();
});
+ fabManager = new FabManager(b.btnAddTransaction);
+
LinearLayoutManager llm = new LinearLayoutManager(this);
llm.setOrientation(RecyclerView.VERTICAL);
@Override
public void onDrawerSlide(@NonNull View drawerView, float slideOffset) {
if (slideOffset > 0.2)
- fabHide();
+ fabManager.hideFab();
}
@Override
public void onDrawerClosed(View drawerView) {
super.onDrawerOpened(drawerView);
mProfileListAdapter.setAnimationsEnabled(true);
Data.drawerOpen.setValue(true);
- fabHide();
+ fabManager.hideFab();
}
};
b.drawerLayout.addDrawerListener(drawerListener);
mainModel.scheduleTransactionListRetrieval();
}
}
- private void createShortcuts(List<MobileLedgerProfile> list) {
+ private void createShortcuts(List<Profile> list) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N_MR1)
return;
ShortcutManager sm = getSystemService(ShortcutManager.class);
List<ShortcutInfo> shortcuts = new ArrayList<>();
int i = 0;
- for (MobileLedgerProfile p : list) {
+ for (Profile p : list) {
if (shortcuts.size() >= sm.getMaxShortcutCountPerActivity())
break;
- if (!p.isPostingPermitted())
+ if (!p.permitPosting())
continue;
final ShortcutInfo.Builder builder =
- new ShortcutInfo.Builder(this, "new_transaction_" + p.getUuid());
+ new ShortcutInfo.Builder(this, "new_transaction_" + p.getId());
ShortcutInfo si = builder.setShortLabel(p.getName())
.setIcon(Icon.createWithResource(this,
R.drawable.thick_plus_icon))
.setIntent(new Intent(Intent.ACTION_VIEW, null, this,
- NewTransactionActivity.class).putExtra("profile_uuid",
- p.getUuid()))
+ NewTransactionActivity.class).putExtra(
+ ProfileThemedActivity.PARAM_PROFILE_ID, p.getId())
+ .putExtra(
+ ProfileThemedActivity.PARAM_THEME,
+ p.getTheme()))
.setRank(i)
.build();
shortcuts.add(si);
}
sm.setDynamicShortcuts(shortcuts);
}
- private void onProfileListChanged(List<MobileLedgerProfile> newList) {
+ private void onProfileListChanged(List<Profile> newList) {
if ((newList == null) || newList.isEmpty()) {
b.noProfilesLayout.setVisibility(View.VISIBLE);
b.mainAppLayout.setVisibility(View.GONE);
(int) (getResources().getDimension(R.dimen.thumb_row_height) * newList.size()));
Logger.debug("profiles", "profile list changed");
- mProfileListAdapter.notifyDataSetChanged();
+ mProfileListAdapter.setProfileList(newList);
createShortcuts(newList);
+
+ final Profile currentProfile = Data.getProfile();
+ Profile replacementProfile = null;
+ if (currentProfile != null) {
+ for (Profile p : newList) {
+ if (p.getId() == currentProfile.getId()) {
+ replacementProfile = p;
+ break;
+ }
+ }
+ }
+
+ if (replacementProfile == null) {
+ Logger.debug(TAG, "Switching profile because the current is no longer available");
+ Data.setCurrentProfile(newList.get(0));
+ }
+ else {
+ Data.setCurrentProfile(replacementProfile);
+ }
}
/**
* called when the current profile has changed
*/
- private void onProfileChanged(MobileLedgerProfile profile) {
- if (this.profile == null) {
- if (profile == null)
- return;
- }
- else {
- if (this.profile.equals(profile))
+ private void onProfileChanged(@Nullable Profile newProfile) {
+ if (this.profile != null) {
+ if (this.profile.equals(newProfile))
return;
}
- boolean haveProfile = profile != null;
+ boolean haveProfile = newProfile != null;
if (haveProfile)
- setTitle(profile.getName());
+ setTitle(newProfile.getName());
else
setTitle(R.string.app_name);
- mainModel.setProfile(profile);
-
- this.profile = profile;
-
- int newProfileTheme = haveProfile ? profile.getThemeHue() : -1;
+ int newProfileTheme = haveProfile ? newProfile.getTheme() : Colors.DEFAULT_HUE_DEG;
if (newProfileTheme != Colors.profileThemeId) {
Logger.debug("profiles",
String.format(Locale.ENGLISH, "profile theme %d → %d", Colors.profileThemeId,
return;
}
+ final boolean sameProfileId = (newProfile != null) && (this.profile != null) &&
+ this.profile.getId() == newProfile.getId();
+
+ this.profile = newProfile;
+
b.noProfilesLayout.setVisibility(haveProfile ? View.GONE : View.VISIBLE);
b.pagerLayout.setVisibility(haveProfile ? View.VISIBLE : View.VISIBLE);
mProfileListAdapter.notifyDataSetChanged();
- mainModel.clearAccounts();
- mainModel.clearTransactions();
-
if (haveProfile) {
- mainModel.scheduleAccountListReload();
- Logger.debug("transactions", "requesting list reload");
- mainModel.scheduleTransactionListReload();
-
- if (profile.isPostingPermitted()) {
+ if (newProfile.permitPosting()) {
b.toolbar.setSubtitle(null);
b.btnAddTransaction.show();
}
}
updateLastUpdateTextFromDB();
+
+ if (sameProfileId) {
+ Logger.debug(TAG, String.format(Locale.ROOT, "Short-cut profile 'changed' to %d",
+ newProfile.getId()));
+ return;
+ }
+
+ mainModel.getAccountFilter()
+ .observe(this, this::onAccountFilterChanged);
+
+ mainModel.stopTransactionsRetrieval();
+ mainModel.clearTransactions();
+ }
+ private void onAccountFilterChanged(String accFilter) {
+ Logger.debug(TAG, "account filter changed, reloading transactions");
+// mainModel.scheduleTransactionListReload();
+ LiveData<List<TransactionWithAccounts>> transactions =
+ new MutableLiveData<>(new ArrayList<>());
+ if (profile != null) {
+ if (accFilter == null || accFilter.isEmpty()) {
+ transactions = DB.get()
+ .getTransactionDAO()
+ .getAllWithAccounts(profile.getId());
+ }
+ else {
+ transactions = DB.get()
+ .getTransactionDAO()
+ .getAllWithAccountsFiltered(profile.getId(), accFilter);
+ }
+ }
+
+ transactions.observe(this, list -> {
+ Logger.debug(TAG,
+ String.format(Locale.ROOT, "got transaction list from DB (%d transactions)",
+ list.size()));
+
+ if (converterThread != null)
+ converterThread.interrupt();
+ converterThread = new ConverterThread(mainModel, list, accFilter);
+ converterThread.start();
+ });
}
private void profileThemeChanged() {
// un-hook all observed LiveData
Data.lastUpdateAccountCount.removeObservers(this);
Data.lastUpdateDate.removeObservers(this);
+ Logger.debug(TAG, "profileThemeChanged(): recreating activity");
recreate();
}
public void fabNewTransactionClicked(View view) {
Intent intent = new Intent(this, NewTransactionActivity.class);
+ intent.putExtra(ProfileThemedActivity.PARAM_PROFILE_ID, profile.getId());
+ intent.putExtra(ProfileThemedActivity.PARAM_THEME, profile.getTheme());
startActivity(intent);
overridePendingTransition(R.anim.slide_in_up, R.anim.dummy);
}
mBackMeansToAccountList = false;
}
else {
- Logger.debug("fragments", String.format(Locale.ENGLISH, "manager stack: %d",
+ Logger.debug(TAG, String.format(Locale.ENGLISH, "manager stack: %d",
getSupportFragmentManager().getBackStackEntryCount()));
super.onBackPressed();
if (profile == null)
return;
- long lastUpdate = profile.getLongOption(MLDB.OPT_LAST_SCRAPE, 0L);
-
- Logger.debug("transactions", String.format(Locale.ENGLISH, "Last update = %d", lastUpdate));
- if (lastUpdate == 0) {
- Data.lastUpdateDate.postValue(null);
- }
- else {
- Data.lastUpdateDate.postValue(new Date(lastUpdate));
- }
-
- scheduleDataRetrievalIfStale(lastUpdate);
-
+ DB.get()
+ .getOptionDAO()
+ .load(profile.getId(), Option.OPT_LAST_SCRAPE)
+ .observe(this, opt -> {
+ long lastUpdate = 0;
+ if (opt != null) {
+ try {
+ lastUpdate = Long.parseLong(opt.getValue());
+ }
+ catch (NumberFormatException ex) {
+ Logger.debug(TAG, String.format("Error parsing '%s' as long", opt.getValue()),
+ ex);
+ }
+ }
+
+ if (lastUpdate == 0) {
+ Data.lastUpdateDate.postValue(null);
+ }
+ else {
+ Data.lastUpdateDate.postValue(new Date(lastUpdate));
+ }
+
+ scheduleDataRetrievalIfStale(lastUpdate);
+ });
}
private void refreshLastUpdateInfo() {
final int formatFlags = DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_SHOW_YEAR |
Integer transactionCount = Data.lastUpdateTransactionCount.getValue();
Date lastUpdate = Data.lastUpdateDate.getValue();
if (lastUpdate == null) {
- Data.lastTransactionsUpdateText.set("----");
- Data.lastAccountsUpdateText.set("----");
+ Data.lastTransactionsUpdateText.setValue("----");
+ Data.lastAccountsUpdateText.setValue("----");
}
else {
- Data.lastTransactionsUpdateText.set(
+ Data.lastTransactionsUpdateText.setValue(
String.format(Objects.requireNonNull(Data.locale.getValue()),
templateForTransactions,
transactionCount == null ? 0 : transactionCount,
DateUtils.formatDateTime(this, lastUpdate.getTime(), formatFlags)));
- Data.lastAccountsUpdateText.set(
+ Data.lastAccountsUpdateText.setValue(
String.format(Objects.requireNonNull(Data.locale.getValue()),
templateForAccounts, accountCount == null ? 0 : accountCount,
DateUtils.formatDateTime(this, lastUpdate.getTime(), formatFlags)));
}
}
public void onStopTransactionRefreshClick(View view) {
- Logger.debug("interactive", "Cancelling transactions refresh");
+ Logger.debug(TAG, "Cancelling transactions refresh");
mainModel.stopTransactionsRetrieval();
b.transactionListCancelDownload.setEnabled(false);
}
b.transactionProgressLayout.setVisibility(View.GONE);
}
}
- public void onRetrieveProgress(RetrieveTransactionsTask.Progress progress) {
- if (progress.getState() == RetrieveTransactionsTask.ProgressState.FINISHED) {
- Logger.debug("progress", "Done");
+ public void onRetrieveProgress(@Nullable RetrieveTransactionsTask.Progress progress) {
+ if (progress == null ||
+ progress.getState() == RetrieveTransactionsTask.ProgressState.FINISHED)
+ {
+ Logger.debug(TAG, "progress: Done");
b.transactionProgressLayout.setVisibility(View.GONE);
mainModel.transactionRetrievalDone();
- String error = progress.getError();
+ String error = (progress == null) ? null : progress.getError();
if (error != null) {
if (error.equals(RetrieveTransactionsTask.Result.ERR_JSON_PARSER_ERROR))
error = getResources().getString(R.string.err_json_parser_error);
AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setMessage(error);
builder.setPositiveButton(R.string.btn_profile_options, (dialog, which) -> {
- Logger.debug("error", "will start profile editor");
- MobileLedgerProfile.startEditProfileActivity(this, profile);
+ Logger.debug(TAG, "will start profile editor");
+ ProfileDetailActivity.start(this, profile);
});
builder.create()
.show();
if (progress.isIndeterminate() || (progress.getTotal() <= 0)) {
b.transactionListProgressBar.setIndeterminate(true);
- Logger.debug("progress", "indeterminate");
+ Logger.debug(TAG, "progress: indeterminate");
}
else {
if (b.transactionListProgressBar.isIndeterminate()) {
b.transactionListProgressBar.setIndeterminate(false);
}
-// Logger.debug("progress",
-// String.format(Locale.US, "%d/%d", progress.getProgress(), progress.getTotal
+// Logger.debug(TAG,
+// String.format(Locale.US, "progress: %d/%d", progress.getProgress(),
+// progress.getTotal
// ()));
b.transactionListProgressBar.setMax(progress.getTotal());
// for some reason animation doesn't work - no progress is shown (stick at 0)
}
}
public void fabShouldShow() {
- if ((profile != null) && profile.isPostingPermitted() && !b.drawerLayout.isOpen())
- b.btnAddTransaction.show();
- else
- fabHide();
+ if ((profile != null) && profile.permitPosting() && !b.drawerLayout.isOpen())
+ fabManager.showFab();
}
- public void fabHide() {
- b.btnAddTransaction.hide();
+ @Override
+ public Context getContext() {
+ return this;
+ }
+ @Override
+ public void showManagedFab() {
+ fabShouldShow();
+ }
+ @Override
+ public void hideManagedFab() {
+ fabManager.hideFab();
}
-
public static class SectionsPagerAdapter extends FragmentStateAdapter {
public SectionsPagerAdapter(@NonNull FragmentActivity fragmentActivity) {
@NotNull
@Override
public Fragment createFragment(int position) {
- Logger.debug("main",
- String.format(Locale.ENGLISH, "Switching to fragment %d", position));
+ Logger.debug(TAG, String.format(Locale.ENGLISH, "Switching to fragment %d", position));
switch (position) {
case 0:
-// debug("flow", "Creating account summary fragment");
+// debug(TAG, "Creating account summary fragment");
return new AccountSummaryFragment();
case 1:
return new TransactionListFragment();
return 2;
}
}
+
+ static private class ConverterThread extends Thread {
+ private final List<TransactionWithAccounts> list;
+ private final MainModel model;
+ private final String accFilter;
+ public ConverterThread(@NonNull MainModel model,
+ @NonNull List<TransactionWithAccounts> list, String accFilter) {
+ this.model = model;
+ this.list = list;
+ this.accFilter = accFilter;
+ }
+ @Override
+ public void run() {
+ TransactionAccumulator accumulator = new TransactionAccumulator(accFilter);
+
+ for (TransactionWithAccounts tr : list) {
+ if (isInterrupted()) {
+ Logger.debug(TAG, "ConverterThread bailing out on interrupt");
+ return;
+ }
+ accumulator.put(new LedgerTransaction(tr));
+ }
+
+ if (isInterrupted()) {
+ Logger.debug(TAG, "ConverterThread bailing out on interrupt");
+ return;
+ }
+
+ Logger.debug(TAG, "ConverterThread publishing results");
+
+ Misc.onMainThread(() -> accumulator.publishResults(model));
+ }
+ }
}