android:name="net.ktnx.mobileledger.MobileLedgerApplication"
android:theme="@style/AppTheme">
<activity
- android:name=".AccountSummary"
+ android:name=".ui.activity.MainActivity"
android:label="@string/account_summary_title"
android:theme="@style/AppTheme.NoActionBar">
<intent-filter>
</intent-filter>
</activity>
<activity
- android:name=".SettingsActivity"
+ android:name=".ui.activity.SettingsActivity"
android:label="@string/title_activity_settings"
- android:parentActivityName=".AccountSummary">
+ android:parentActivityName=".ui.activity.MainActivity">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
- android:value="net.ktnx.mobileledger.AccountSummary" />
+ android:value="net.ktnx.mobileledger.ui.activity.MainActivity" />
</activity>
<activity
- android:name=".NewTransactionActivity"
+ android:name=".ui.activity.NewTransactionActivity"
android:label="@string/title_activity_new_transaction"
- android:parentActivityName=".AccountSummary"
+ android:parentActivityName=".ui.activity.MainActivity"
android:theme="@style/AppTheme.NoActionBar">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
- android:value="net.ktnx.mobileledger.AccountSummary" />
- </activity>
- <activity
- android:name=".TransactionListActivity"
- android:label="@string/title_activity_transaction_list"
- android:parentActivityName=".AccountSummary"
- android:theme="@style/AppTheme.NoActionBar">
- <meta-data
- android:name="android.support.PARENT_ACTIVITY"
- android:value="net.ktnx.mobileledger.AccountSummary" />
- <intent-filter>
- <action android:name="android.intent.action.MAIN" />
- <category android:name="android.intent.category.LAUNCHER" />
- </intent-filter>
+ android:value="net.ktnx.mobileledger.ui.activity.MainActivity" />
</activity>
</application>
+++ /dev/null
-/*
- * Copyright © 2018 Damyan Ivanov.
- * This file is part of Mobile-Ledger.
- * Mobile-Ledger is free software: you can distribute it and/or modify it
- * under the term of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your opinion), any later version.
- *
- * Mobile-Ledger is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License terms for details.
- *
- * You should have received a copy of the GNU General Public License
- * along with Mobile-Ledger. If not, see <https://www.gnu.org/licenses/>.
- */
-
-package net.ktnx.mobileledger;
-
-import android.arch.lifecycle.ViewModelProviders;
-import android.content.Intent;
-import android.content.SharedPreferences;
-import android.content.pm.PackageInfo;
-import android.os.Build;
-import android.os.Bundle;
-import android.preference.PreferenceManager;
-import android.support.annotation.NonNull;
-import android.support.design.widget.FloatingActionButton;
-import android.support.design.widget.Snackbar;
-import android.support.v4.view.GravityCompat;
-import android.support.v4.widget.DrawerLayout;
-import android.support.v4.widget.SwipeRefreshLayout;
-import android.support.v7.app.ActionBarDrawerToggle;
-import android.support.v7.app.AppCompatActivity;
-import android.support.v7.widget.LinearLayoutManager;
-import android.support.v7.widget.RecyclerView;
-import android.support.v7.widget.Toolbar;
-import android.util.Log;
-import android.view.Menu;
-import android.view.MenuItem;
-import android.view.View;
-import android.widget.LinearLayout;
-
-import net.ktnx.mobileledger.async.RetrieveAccountsTask;
-import net.ktnx.mobileledger.model.LedgerAccount;
-import net.ktnx.mobileledger.utils.MLDB;
-
-import java.lang.ref.WeakReference;
-import java.util.Date;
-import java.util.List;
-
-import static net.ktnx.mobileledger.SettingsActivity.PREF_KEY_SHOW_ONLY_STARRED_ACCOUNTS;
-
-public class AccountSummary extends AppCompatActivity {
- private static long account_list_last_updated;
- private static boolean account_list_needs_update = true;
- DrawerLayout drawer;
- MenuItem mShowHiddenAccounts;
- SharedPreferences.OnSharedPreferenceChangeListener sBindPreferenceSummaryToValueListener;
- private AccountSummaryViewModel model;
- private AccountSummaryAdapter modelAdapter;
- private Menu optMenu;
-
- public static void preferences_changed() {
- account_list_needs_update = true;
- }
-
- @Override
- protected void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- setContentView(R.layout.activity_account_summary);
- Toolbar toolbar = findViewById(R.id.toolbar);
- setSupportActionBar(toolbar);
-
- drawer = findViewById(R.id.drawer_layout);
- ActionBarDrawerToggle toggle =
- new ActionBarDrawerToggle(this, drawer, toolbar, R.string.navigation_drawer_open,
- R.string.navigation_drawer_close);
- drawer.addDrawerListener(toggle);
- toggle.syncState();
-
- android.widget.TextView ver = drawer.findViewById(R.id.drawer_version_text);
-
- try {
- PackageInfo pi =
- getApplicationContext().getPackageManager().getPackageInfo(getPackageName(), 0);
- ver.setText(pi.versionName);
- }
- catch (Exception e) {
- e.printStackTrace();
- }
-
- model = ViewModelProviders.of(this).get(AccountSummaryViewModel.class);
- List<LedgerAccount> accounts = model.getAccounts(getApplicationContext());
- modelAdapter = new AccountSummaryAdapter(accounts);
-
- RecyclerView root = findViewById(R.id.account_root);
- root.setAdapter(modelAdapter);
-
- LinearLayoutManager llm = new LinearLayoutManager(this);
- llm.setOrientation(LinearLayoutManager.VERTICAL);
- root.setLayoutManager(llm);
-
- root.addOnItemTouchListener(new RecyclerItemListener(this, root,
- new RecyclerItemListener.RecyclerTouchListener() {
- @Override
- public void onClickItem(View v, int position) {
- Log.d("list", String.format("item %d clicked", position));
- if (modelAdapter.isSelectionActive()) {
- modelAdapter.selectItem(position);
- }
- }
-
- @Override
- public void onLongClickItem(View v, int position) {
- Log.d("list", String.format("item %d long-clicked", position));
- modelAdapter.startSelection();
- if (optMenu != null) {
- optMenu.findItem(R.id.menu_acc_summary_cancel_selection)
- .setVisible(true);
- optMenu.findItem(R.id.menu_acc_summary_confirm_selection)
- .setVisible(true);
- optMenu.findItem(R.id.menu_acc_summary_only_starred).setVisible(false);
- }
- {
- FloatingActionButton fab = findViewById(R.id.btn_add_transaction);
- if (fab != null) fab.hide();
- }
- }
- }));
-
- root.addOnScrollListener(new RecyclerView.OnScrollListener() {
- @Override
- public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
- if (dy < 0) ((FloatingActionButton) findViewById(R.id.btn_add_transaction)).show();
- if (dy > 0) ((FloatingActionButton) findViewById(R.id.btn_add_transaction)).hide();
- }
- });
- SwipeRefreshLayout swiper = findViewById(R.id.account_swiper);
- swiper.setColorSchemeResources(R.color.colorPrimary, R.color.colorAccent);
- swiper.setOnRefreshListener(() -> {
- Log.d("ui", "refreshing accounts via swipe");
- update_accounts(true);
- });
- prepare_db();
-// update_account_table();
- update_accounts(false);
- }
-
- @Override
- protected void onStart() {
- super.onStart();
- LinearLayout grp = drawer.findViewById(R.id.nav_actions);
- for (int i = 0; i < grp.getChildCount(); i++) {
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
- grp.getChildAt(i).setBackgroundColor(
- getResources().getColor(R.color.drawer_background, getTheme()));
- }
- else {
- grp.getChildAt(i)
- .setBackgroundColor(getResources().getColor(R.color.drawer_background));
- }
- }
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
- drawer.findViewById(R.id.nav_account_summary).setBackgroundColor(
- getResources().getColor(R.color.table_row_even_bg, getTheme()));
- }
- else {
- drawer.findViewById(R.id.nav_account_summary)
- .setBackgroundColor(getResources().getColor(R.color.table_row_even_bg));
- }
- }
-
- public void fab_new_transaction_clicked(View view) {
- Intent intent = new Intent(this, NewTransactionActivity.class);
- startActivity(intent);
- overridePendingTransition(R.anim.slide_in_right, R.anim.dummy);
- }
-
- public void nav_exit_clicked(View view) {
- Log.w("app", "exiting");
- finish();
- }
-
- public void nav_settings_clicked(View view) {
- Intent intent = new Intent(this, SettingsActivity.class);
- startActivity(intent);
- }
- public void onLatestTransactionsClicked(View view) {
- Intent intent = new Intent(this, TransactionListActivity.class);
- startActivity(intent);
- }
- @Override
- public void onBackPressed() {
- DrawerLayout drawer = findViewById(R.id.drawer_layout);
- if (drawer.isDrawerOpen(GravityCompat.START)) {
- drawer.closeDrawer(GravityCompat.START);
- }
- else {
- super.onBackPressed();
- }
- }
-
- @Override
- public boolean onCreateOptionsMenu(Menu menu) {
- // Inflate the menu; this adds items to the action bar if it is present.
- getMenuInflater().inflate(R.menu.account_summary, menu);
- optMenu = menu;
-
- mShowHiddenAccounts = menu.findItem(R.id.menu_acc_summary_only_starred);
- if (mShowHiddenAccounts == null) throw new AssertionError();
-
- sBindPreferenceSummaryToValueListener = (preference, value) -> mShowHiddenAccounts
- .setChecked(preference.getBoolean(PREF_KEY_SHOW_ONLY_STARRED_ACCOUNTS, false));
- SharedPreferences pref = PreferenceManager.getDefaultSharedPreferences(this);
- pref.registerOnSharedPreferenceChangeListener(sBindPreferenceSummaryToValueListener);
-
- mShowHiddenAccounts.setChecked(pref.getBoolean(PREF_KEY_SHOW_ONLY_STARRED_ACCOUNTS, false));
-
- return true;
- }
-
- @Override
- public boolean onOptionsItemSelected(MenuItem item) {
- // Handle action bar item clicks here. The action bar will
- // automatically handle clicks on the Home/Up button, so long
- // as you specify a parent activity in AndroidManifest.xml.
-// int id = item.getItemId();
-
- //noinspection SimplifiableIfStatement
- //if (id == R.id.action_settings) {
- // return true;
- // }
-
- return super.onOptionsItemSelected(item);
- }
-
- public void onRefreshAccountSummaryClicked(MenuItem mi) {
- update_accounts(true);
- }
-
- public void onShowOnlyStarredClicked(MenuItem mi) {
- SharedPreferences pref = PreferenceManager.getDefaultSharedPreferences(this);
- boolean flag = pref.getBoolean(PREF_KEY_SHOW_ONLY_STARRED_ACCOUNTS, false);
-
- SharedPreferences.Editor editor = pref.edit();
- editor.putBoolean(PREF_KEY_SHOW_ONLY_STARRED_ACCOUNTS, !flag);
- Log.d("pref", "Setting show only starred accounts pref to " + (flag ? "false" : "true"));
- editor.apply();
-
- update_account_table();
- }
-
- private void prepare_db() {
- account_list_last_updated = MLDB.get_option_value(this, "last_refresh", (long) 0);
- }
-
- private void update_accounts(boolean force) {
- long now = new Date().getTime();
- if ((now > (account_list_last_updated + (24 * 3600 * 1000))) || force) {
- Log.d("db",
- "accounts last updated at " + account_list_last_updated + " and now is " + now +
- ". re-fetching");
- update_accounts();
- }
- }
-
- private void update_accounts() {
- RetrieveAccountsTask task = new RetrieveAccountsTask(new WeakReference<>(this));
-
- task.setPref(PreferenceManager.getDefaultSharedPreferences(this));
- task.execute();
-
- }
- public void onAccountRefreshDone(int error) {
- SwipeRefreshLayout srl = findViewById(R.id.account_swiper);
- srl.setRefreshing(false);
- if (error != 0) {
- String err_text = getResources().getString(error);
- Log.d("visual", String.format("showing snackbar: %s", err_text));
- Snackbar.make(drawer, err_text, Snackbar.LENGTH_LONG).show();
- }
- else {
- MLDB.set_option_value(this, "last_refresh", new Date().getTime());
- update_account_table();
- }
- }
- private void update_account_table() {
- model.reloadAccounts(getApplicationContext());
- modelAdapter.notifyDataSetChanged();
- }
- void stopSelection() {
- modelAdapter.stopSelection();
- if (optMenu != null) {
- optMenu.findItem(R.id.menu_acc_summary_cancel_selection).setVisible(false);
- optMenu.findItem(R.id.menu_acc_summary_confirm_selection).setVisible(false);
- optMenu.findItem(R.id.menu_acc_summary_only_starred).setVisible(true);
- }
- {
- FloatingActionButton fab = findViewById(R.id.btn_add_transaction);
- if (fab != null) fab.show();
- }
- }
- public void onCancelAccSelection(MenuItem item) {
- stopSelection();
- }
- public void onConfirmAccSelection(MenuItem item) {
- model.commitSelections(getApplicationContext());
- stopSelection();
- }
-}
+++ /dev/null
-/*
- * Copyright © 2018 Damyan Ivanov.
- * This file is part of Mobile-Ledger.
- * Mobile-Ledger is free software: you can distribute it and/or modify it
- * under the term of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your opinion), any later version.
- *
- * Mobile-Ledger is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License terms for details.
- *
- * You should have received a copy of the GNU General Public License
- * along with Mobile-Ledger. If not, see <https://www.gnu.org/licenses/>.
- */
-
-package net.ktnx.mobileledger;
-
-import android.app.Application;
-import android.arch.lifecycle.AndroidViewModel;
-import android.content.Context;
-import android.content.res.Resources;
-import android.database.Cursor;
-import android.database.sqlite.SQLiteDatabase;
-import android.graphics.Typeface;
-import android.os.Build;
-import android.preference.PreferenceManager;
-import android.support.annotation.NonNull;
-import android.support.v7.widget.RecyclerView;
-import android.util.Log;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.CheckBox;
-import android.widget.LinearLayout;
-import android.widget.TextView;
-
-import net.ktnx.mobileledger.model.LedgerAccount;
-import net.ktnx.mobileledger.utils.MLDB;
-
-import java.util.ArrayList;
-import java.util.List;
-
-import static net.ktnx.mobileledger.SettingsActivity.PREF_KEY_SHOW_ONLY_STARRED_ACCOUNTS;
-
-class AccountSummaryViewModel extends AndroidViewModel {
- private List<LedgerAccount> accounts;
-
- public AccountSummaryViewModel(@NonNull Application application) {
- super(application);
- }
-
- List<LedgerAccount> getAccounts(Context context) {
- if (accounts == null) {
- accounts = new ArrayList<>();
- reloadAccounts(context);
- }
-
- return accounts;
- }
-
- void reloadAccounts(Context context) {
- accounts.clear();
- boolean showingOnlyStarred =
- PreferenceManager.getDefaultSharedPreferences(getApplication())
- .getBoolean(PREF_KEY_SHOW_ONLY_STARRED_ACCOUNTS, false);
- String sql = "SELECT name, hidden FROM accounts";
- if (showingOnlyStarred) sql += " WHERE hidden = 0";
- sql += " ORDER BY name";
-
- try (SQLiteDatabase db = MLDB.getReadableDatabase(context)) {
- try (Cursor cursor = db
- .rawQuery(sql,null))
- {
- while (cursor.moveToNext()) {
- LedgerAccount acc = new LedgerAccount(cursor.getString(0));
- acc.setHidden(cursor.getInt(1) == 1);
- try (Cursor c2 = db.rawQuery(
- "SELECT value, currency FROM account_values " + "WHERE account = ?",
- new String[]{acc.getName()}))
- {
- while (c2.moveToNext()) {
- acc.addAmount(c2.getFloat(0), c2.getString(1));
- }
- }
- accounts.add(acc);
- }
- }
- }
- }
- void commitSelections(Context context) {
- try(SQLiteDatabase db = MLDB.getWritableDatabase(context)) {
- db.beginTransaction();
- try {
- for (LedgerAccount acc : accounts) {
- Log.d("db", String.format("Setting %s to %s", acc.getName(),
- acc.isHidden() ? "hidden" : "starred"));
- db.execSQL("UPDATE accounts SET hidden=? WHERE name=?",
- new Object[]{acc.isHiddenToBe() ? 1 : 0, acc.getName()});
- }
- db.setTransactionSuccessful();
- for (LedgerAccount acc : accounts ) { acc.setHidden(acc.isHiddenToBe()); }
- }
- finally { db.endTransaction(); }
- }
- }
-}
-
-class AccountSummaryAdapter extends RecyclerView.Adapter<AccountSummaryAdapter.LedgerRowHolder> {
- private List<LedgerAccount> accounts;
- private boolean selectionActive;
-
- AccountSummaryAdapter(List<LedgerAccount> accounts) {
- this.accounts = accounts;
- this.selectionActive = false;
- }
-
- public void onBindViewHolder(@NonNull LedgerRowHolder holder, int position) {
- LedgerAccount acc = accounts.get(position);
- Context ctx = holder.row.getContext();
- Resources rm = ctx.getResources();
-
- holder.tvAccountName.setText(acc.getShortName());
- holder.tvAccountName.setPadding(
- acc.getLevel() * rm.getDimensionPixelSize(R.dimen.activity_horizontal_margin)/2,
- 0, 0,
- 0);
- holder.tvAccountAmounts.setText(acc.getAmountsString());
-
- if (acc.isHidden()) {
- holder.tvAccountName.setTypeface(null, Typeface.ITALIC);
- holder.tvAccountAmounts.setTypeface(null, Typeface.ITALIC);
- }
- else {
- holder.tvAccountName.setTypeface(null, Typeface.NORMAL);
- holder.tvAccountAmounts.setTypeface(null, Typeface.NORMAL);
- }
-
- if (position % 2 == 0) {
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) holder.row
- .setBackgroundColor(rm.getColor(R.color.table_row_even_bg, ctx.getTheme()));
- else holder.row.setBackgroundColor(rm.getColor(R.color.table_row_even_bg));
- }
- else {
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) holder.row
- .setBackgroundColor(rm.getColor(R.color.drawer_background, ctx.getTheme()));
- else holder.row.setBackgroundColor(rm.getColor(R.color.drawer_background));
- }
-
- holder.selectionCb.setVisibility( selectionActive ? View.VISIBLE : View.GONE);
- holder.selectionCb.setChecked(!acc.isHiddenToBe());
-
- holder.row.setTag(R.id.POS, position);
- }
-
- @NonNull
- @Override
- public LedgerRowHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
- View row = LayoutInflater.from(parent.getContext())
- .inflate(R.layout.account_summary_row, parent, false);
- return new LedgerRowHolder(row);
- }
-
- @Override
- public int getItemCount() {
- return accounts.size();
- }
- public void startSelection() {
- for( LedgerAccount acc : accounts ) acc.setHiddenToBe(acc.isHidden());
- this.selectionActive = true;
- notifyDataSetChanged();
- }
-
- public void stopSelection() {
- this.selectionActive = false;
- notifyDataSetChanged();
- }
-
- public boolean isSelectionActive() {
- return selectionActive;
- }
-
- public void selectItem(int position) {
- LedgerAccount acc = accounts.get(position);
- acc.toggleHiddenToBe();
- toggleChildrenOf(acc, acc.isHiddenToBe());
- notifyDataSetChanged();
- }
- void toggleChildrenOf(LedgerAccount parent, boolean hiddenToBe) {
- for (LedgerAccount acc : accounts) {
- String acc_parent = acc.getParentName();
- if ((acc_parent != null) && acc.getParentName().equals(parent.getName())) {
- acc.setHiddenToBe(hiddenToBe);
- toggleChildrenOf(acc, hiddenToBe);
- }
- }
- }
- class LedgerRowHolder extends RecyclerView.ViewHolder {
- CheckBox selectionCb;
- TextView tvAccountName, tvAccountAmounts;
- LinearLayout row;
- public LedgerRowHolder(@NonNull View itemView) {
- super(itemView);
- this.row = (LinearLayout) itemView;
- this.tvAccountName = itemView.findViewById(R.id.account_row_acc_name);
- this.tvAccountAmounts = itemView.findViewById(R.id.account_row_acc_amounts);
- this.selectionCb = itemView.findViewById(R.id.account_row_check);
- }
- }
-}
\ No newline at end of file
+++ /dev/null
-/*
- * Copyright © 2018 Damyan Ivanov.
- * This file is part of Mobile-Ledger.
- * Mobile-Ledger is free software: you can distribute it and/or modify it
- * under the term of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your opinion), any later version.
- *
- * Mobile-Ledger is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License terms for details.
- *
- * You should have received a copy of the GNU General Public License
- * along with Mobile-Ledger. If not, see <https://www.gnu.org/licenses/>.
- */
-
-package net.ktnx.mobileledger;
-
-import android.content.res.Configuration;
-import android.os.Bundle;
-import android.preference.PreferenceActivity;
-import android.support.annotation.LayoutRes;
-import android.support.annotation.NonNull;
-import android.support.annotation.Nullable;
-import android.support.v7.app.ActionBar;
-import android.support.v7.app.AppCompatDelegate;
-import android.support.v7.widget.Toolbar;
-import android.view.MenuInflater;
-import android.view.View;
-import android.view.ViewGroup;
-
-/**
- * A {@link android.preference.PreferenceActivity} which implements and proxies the necessary calls
- * to be used with AppCompat.
- */
-public abstract class AppCompatPreferenceActivity extends PreferenceActivity {
-
- private AppCompatDelegate mDelegate;
-
- @Override
- protected void onCreate(Bundle savedInstanceState) {
- getDelegate().installViewFactory();
- getDelegate().onCreate(savedInstanceState);
- super.onCreate(savedInstanceState);
- }
-
- @Override
- protected void onPostCreate(Bundle savedInstanceState) {
- super.onPostCreate(savedInstanceState);
- getDelegate().onPostCreate(savedInstanceState);
- }
-
- public ActionBar getSupportActionBar() {
- return getDelegate().getSupportActionBar();
- }
-
- public void setSupportActionBar(@Nullable Toolbar toolbar) {
- getDelegate().setSupportActionBar(toolbar);
- }
-
- @NonNull
- @Override
- public MenuInflater getMenuInflater() {
- return getDelegate().getMenuInflater();
- }
-
- @Override
- public void setContentView(@LayoutRes int layoutResID) {
- getDelegate().setContentView(layoutResID);
- }
-
- @Override
- public void setContentView(View view) {
- getDelegate().setContentView(view);
- }
-
- @Override
- public void setContentView(View view, ViewGroup.LayoutParams params) {
- getDelegate().setContentView(view, params);
- }
-
- @Override
- public void addContentView(View view, ViewGroup.LayoutParams params) {
- getDelegate().addContentView(view, params);
- }
-
- @Override
- protected void onPostResume() {
- super.onPostResume();
- getDelegate().onPostResume();
- }
-
- @Override
- protected void onTitleChanged(CharSequence title, int color) {
- super.onTitleChanged(title, color);
- getDelegate().setTitle(title);
- }
-
- @Override
- public void onConfigurationChanged(Configuration newConfig) {
- super.onConfigurationChanged(newConfig);
- getDelegate().onConfigurationChanged(newConfig);
- }
-
- @Override
- protected void onStop() {
- super.onStop();
- getDelegate().onStop();
- }
-
- @Override
- protected void onDestroy() {
- super.onDestroy();
- getDelegate().onDestroy();
- }
-
- public void invalidateOptionsMenu() {
- getDelegate().invalidateOptionsMenu();
- }
-
- private AppCompatDelegate getDelegate() {
- if (mDelegate == null) {
- mDelegate = AppCompatDelegate.create(this, null);
- }
- return mDelegate;
- }
-}
+++ /dev/null
-/*
- * Copyright © 2018 Damyan Ivanov.
- * This file is part of Mobile-Ledger.
- * Mobile-Ledger is free software: you can distribute it and/or modify it
- * under the term of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your opinion), any later version.
- *
- * Mobile-Ledger is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License terms for details.
- *
- * You should have received a copy of the GNU General Public License
- * along with Mobile-Ledger. If not, see <https://www.gnu.org/licenses/>.
- */
-
-package net.ktnx.mobileledger;
-
-import android.annotation.SuppressLint;
-import android.os.Bundle;
-import android.preference.PreferenceManager;
-import android.support.design.widget.BaseTransientBottomBar;
-import android.support.design.widget.Snackbar;
-import android.support.v4.app.DialogFragment;
-import android.support.v7.app.AppCompatActivity;
-import android.support.v7.widget.Toolbar;
-import android.text.Editable;
-import android.text.InputType;
-import android.text.TextWatcher;
-import android.util.Log;
-import android.util.TypedValue;
-import android.view.Gravity;
-import android.view.Menu;
-import android.view.MenuItem;
-import android.view.MotionEvent;
-import android.view.View;
-import android.view.inputmethod.EditorInfo;
-import android.widget.AutoCompleteTextView;
-import android.widget.EditText;
-import android.widget.ProgressBar;
-import android.widget.TableLayout;
-import android.widget.TableRow;
-import android.widget.TextView;
-
-import net.ktnx.mobileledger.async.SaveTransactionTask;
-import net.ktnx.mobileledger.async.TaskCallback;
-import net.ktnx.mobileledger.model.LedgerTransaction;
-import net.ktnx.mobileledger.model.LedgerTransactionAccount;
-import net.ktnx.mobileledger.ui.DatePickerFragment;
-import net.ktnx.mobileledger.utils.MLDB;
-
-import java.util.Date;
-import java.util.Objects;
-
-/*
- * TODO: nicer progress while transaction is submitted
- * TODO: latest transactions, maybe with browsing further in the past?
- * TODO: reports
- * TODO: get rid of the custom session/cookie and auth code?
- * (the last problem with the POST was the missing content-length header)
- * TODO: app icon
- * TODO: nicer swiping removal with visual feedback
- * TODO: setup wizard
- * TODO: update accounts/check settings upon change of backend settings
- * */
-
-public class NewTransactionActivity extends AppCompatActivity implements TaskCallback {
- private static SaveTransactionTask saver;
- private TableLayout table;
- private ProgressBar progress;
- private TextView text_date;
- private AutoCompleteTextView text_descr;
- private MenuItem mSave;
-
- @Override
- protected void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- setContentView(R.layout.activity_new_transaction);
- Toolbar toolbar = findViewById(R.id.toolbar);
- setSupportActionBar(toolbar);
-
- text_date = findViewById(R.id.new_transaction_date);
- text_date.setOnFocusChangeListener(new View.OnFocusChangeListener() {
- @Override
- public void onFocusChange(View v, boolean hasFocus) {
- if (hasFocus) pickTransactionDate(v);
- }
- });
- text_descr = findViewById(R.id.new_transaction_description);
- MLDB.hook_autocompletion_adapter(this, text_descr, MLDB.DESCRIPTION_HISTORY_TABLE,
- "description");
- hook_text_change_listener(text_descr);
-
- progress = findViewById(R.id.save_transaction_progress);
-
- Objects.requireNonNull(getSupportActionBar()).setDisplayHomeAsUpEnabled(true);
- table = findViewById(R.id.new_transaction_accounts_table);
- for (int i = 0; i < table.getChildCount(); i++) {
- TableRow row = (TableRow) table.getChildAt(i);
- AutoCompleteTextView acc_name_view = (AutoCompleteTextView) row.getChildAt(0);
- TextView amount_view = (TextView) row.getChildAt(1);
- hook_swipe_listener(row);
- MLDB.hook_autocompletion_adapter(this, acc_name_view, MLDB.ACCOUNTS_TABLE, "name");
- hook_text_change_listener(acc_name_view);
- hook_text_change_listener(amount_view);
-// Log.d("swipe", "hooked to row "+i);
- }
- }
-
- @Override
- protected void onStart() {
- super.onStart();
- if (text_descr.getText().toString().isEmpty()) text_descr.requestFocus();
- }
-
- @Override
- public void finish() {
- super.finish();
- overridePendingTransition(R.anim.dummy, R.anim.slide_out_right);
- }
-
- @Override
- public boolean onOptionsItemSelected(MenuItem item) {
- switch (item.getItemId()) {
- case android.R.id.home:
- finish();
- return true;
- }
- return super.onOptionsItemSelected(item);
- }
-
- public void save_transaction() {
- if (mSave != null) mSave.setVisible(false);
- toggle_all_editing(false);
- progress.setVisibility(View.VISIBLE);
-
- saver = new SaveTransactionTask(this);
-
- saver.setPref(PreferenceManager.getDefaultSharedPreferences(this));
- String date = text_date.getText().toString();
- if (date.isEmpty()) date = String.valueOf(new Date().getDate());
- LedgerTransaction tr = new LedgerTransaction(date, text_descr.getText().toString());
-
- TableLayout table = findViewById(R.id.new_transaction_accounts_table);
- for (int i = 0; i < table.getChildCount(); i++) {
- TableRow row = (TableRow) table.getChildAt(i);
- String acc = ((TextView) row.getChildAt(0)).getText().toString();
- String amt = ((TextView) row.getChildAt(1)).getText().toString();
- LedgerTransactionAccount item =
- amt.length() > 0 ? new LedgerTransactionAccount(acc, Float.parseFloat(amt))
- : new LedgerTransactionAccount(acc);
-
- tr.addAccount(item);
- }
- saver.execute(tr);
- }
-
- private void toggle_all_editing(boolean enabled) {
- text_date.setEnabled(enabled);
- text_descr.setEnabled(enabled);
- TableLayout table = findViewById(R.id.new_transaction_accounts_table);
- for (int i = 0; i < table.getChildCount(); i++) {
- TableRow row = (TableRow) table.getChildAt(i);
- for (int j = 0; j < row.getChildCount(); j++) {
- row.getChildAt(j).setEnabled(enabled);
- }
- }
- }
-
- private void hook_swipe_listener(final TableRow row) {
- row.getChildAt(0).setOnTouchListener(new OnSwipeTouchListener(this) {
- public void onSwipeLeft() {
-// Log.d("swipe", "LEFT" + row.getId());
- if (table.getChildCount() > 2) {
- TableRow prev_row = (TableRow) table.getChildAt(table.indexOfChild(row) - 1);
- TableRow next_row = (TableRow) table.getChildAt(table.indexOfChild(row) + 1);
- TextView prev_amt =
- (prev_row != null) ? (TextView) prev_row.getChildAt(1) : text_descr;
- TextView next_acc =
- (next_row != null) ? (TextView) next_row.getChildAt(0) : null;
-
- if (next_acc == null) {
- prev_amt.setNextFocusRightId(R.id.none);
- prev_amt.setNextFocusForwardId(R.id.none);
- prev_amt.setImeOptions(EditorInfo.IME_ACTION_DONE);
- }
- else {
- prev_amt.setNextFocusRightId(next_acc.getId());
- prev_amt.setNextFocusForwardId(next_acc.getId());
- prev_amt.setImeOptions(EditorInfo.IME_ACTION_NEXT);
- }
-
- if (row.hasFocus()) {
- if (next_acc != null) next_acc.requestFocus();
- else prev_amt.requestFocus();
- }
-
- table.removeView(row);
- check_transaction_submittable();
-// Toast.makeText(NewTransactionActivity.this, "LEFT", Toast.LENGTH_LONG).show();
- }
- else {
- Snackbar.make(table, R.string.msg_at_least_two_accounts_are_required,
- Snackbar.LENGTH_LONG).setAction("Action", null).show();
- }
- }
- // @Override
-// public boolean performClick(View view, MotionEvent m) {
-// return true;
-// }
- public boolean onTouch(View view, MotionEvent m) {
- return gestureDetector.onTouchEvent(m);
- }
- });
- }
-
- private void hook_text_change_listener(final TextView view) {
- view.addTextChangedListener(new TextWatcher() {
- @Override
- public void beforeTextChanged(CharSequence s, int start, int count, int after) {
-
- }
-
- @Override
- public void onTextChanged(CharSequence s, int start, int before, int count) {
-
- }
-
- @Override
- public void afterTextChanged(Editable s) {
-// Log.d("input", "text changed");
- check_transaction_submittable();
- }
- });
-
- }
-
- public boolean onCreateOptionsMenu(Menu menu) {
- // Inflate the menu; this adds items to the action bar if it is present.
- getMenuInflater().inflate(R.menu.new_transaction, menu);
- mSave = menu.findItem(R.id.action_submit_transaction);
- if (mSave == null) throw new AssertionError();
-
- check_transaction_submittable();
-
- return true;
- }
-
- public void pickTransactionDate(View view) {
- DialogFragment picker = new DatePickerFragment();
- picker.show(getSupportFragmentManager(), "datePicker");
- }
-
- public int dp2px(float dp) {
- return Math.round(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp,
- getResources().getDisplayMetrics()));
- }
-
- private void do_add_account_row(boolean focus) {
- final AutoCompleteTextView acc = new AutoCompleteTextView(this);
- acc.setLayoutParams(new TableRow.LayoutParams(TableRow.LayoutParams.MATCH_PARENT,
- TableRow.LayoutParams.WRAP_CONTENT, 9f));
- acc.setHint(R.string.new_transaction_account_hint);
- acc.setWidth(0);
- acc.setImeOptions(EditorInfo.IME_ACTION_NEXT | EditorInfo.IME_FLAG_NO_ENTER_ACTION |
- EditorInfo.IME_FLAG_NAVIGATE_NEXT);
- acc.setSingleLine(true);
-
- final EditText amt = new EditText(this);
- amt.setLayoutParams(new TableRow.LayoutParams(TableRow.LayoutParams.WRAP_CONTENT,
- TableRow.LayoutParams.MATCH_PARENT, 1f));
- amt.setHint(R.string.new_transaction_amount_hint);
- amt.setWidth(0);
- amt.setInputType(InputType.TYPE_CLASS_NUMBER | InputType.TYPE_NUMBER_FLAG_SIGNED |
- InputType.TYPE_NUMBER_FLAG_DECIMAL);
- amt.setMinWidth(dp2px(40));
- amt.setTextAlignment(EditText.TEXT_ALIGNMENT_VIEW_END);
- amt.setImeOptions(EditorInfo.IME_ACTION_DONE);
-
- // forward navigation support
- final TableRow last_row = (TableRow) table.getChildAt(table.getChildCount() - 1);
- final TextView last_amt = (TextView) last_row.getChildAt(1);
- last_amt.setNextFocusForwardId(acc.getId());
- last_amt.setNextFocusRightId(acc.getId());
- last_amt.setImeOptions(EditorInfo.IME_ACTION_NEXT);
- acc.setNextFocusForwardId(amt.getId());
- acc.setNextFocusRightId(amt.getId());
-
- final TableRow row = new TableRow(this);
- row.setLayoutParams(new TableRow.LayoutParams(TableRow.LayoutParams.MATCH_PARENT,
- TableRow.LayoutParams.MATCH_PARENT));
- row.setGravity(Gravity.BOTTOM);
- row.addView(acc);
- row.addView(amt);
- table.addView(row);
-
- if (focus) acc.requestFocus();
-
- hook_swipe_listener(row);
- MLDB.hook_autocompletion_adapter(this, acc, MLDB.ACCOUNTS_TABLE, "name");
- hook_text_change_listener(acc);
- hook_text_change_listener(amt);
- }
-
- public void addTransactionAccountFromMenu(MenuItem item) {
- do_add_account_row(true);
- }
-
- public void resetTransactionFromMenu(MenuItem item) {
- reset_form();
- }
-
- public void saveTransactionFromMenu(MenuItem item) {
- save_transaction();
- }
-
- private boolean is_zero(float f) {
- return (f < 0.005) && (f > -0.005);
- }
-
- // rules:
- // 1) at least two account names
- // 2) each amount must have account name
- // 3) amounts must balance to 0, or
- // 3a) there must be exactly one empty amount
- // 4) empty accounts with empty amounts are ignored
- // 5) a row with an empty account name or empty amount is guaranteed to exist
- @SuppressLint("DefaultLocale")
- private void check_transaction_submittable() {
- TableLayout table = findViewById(R.id.new_transaction_accounts_table);
- int accounts = 0;
- int accounts_with_values = 0;
- int amounts = 0;
- int amounts_with_accounts = 0;
- int empty_rows = 0;
- TextView empty_amount = null;
- boolean single_empty_amount = false;
- boolean single_empty_amount_has_account = false;
- float running_total = 0f;
- boolean have_description =
- !((TextView) findViewById(R.id.new_transaction_description)).getText().toString()
- .isEmpty();
-
- try {
- for (int i = 0; i < table.getChildCount(); i++) {
- TableRow row = (TableRow) table.getChildAt(i);
-
- TextView acc_name_v = (TextView) row.getChildAt(0);
- TextView amount_v = (TextView) row.getChildAt(1);
- String amt = String.valueOf(amount_v.getText());
- String acc_name = String.valueOf(acc_name_v.getText());
- acc_name = acc_name.trim();
-
- if (!acc_name.isEmpty()) {
- accounts++;
-
- if (!amt.isEmpty()) {
- accounts_with_values++;
- }
- }
- else empty_rows++;
-
- if (amt.isEmpty()) {
- amount_v.setHint(String.format("%1.2f", 0f));
- if (empty_amount == null) {
- empty_amount = amount_v;
- single_empty_amount = true;
- single_empty_amount_has_account = !acc_name.isEmpty();
- }
- else if (!acc_name.isEmpty()) single_empty_amount = false;
- }
- else {
- amounts++;
- if (!acc_name.isEmpty()) amounts_with_accounts++;
- running_total += Float.valueOf(amt);
- }
- }
-
- if ((empty_rows == 0) &&
- ((table.getChildCount() == accounts) || (table.getChildCount() == amounts)))
- {
- do_add_account_row(false);
- }
-
- Log.d("submittable", String.format("accounts=%d, accounts_with_values=%s, " +
- "amounts_with_accounts=%d, amounts=%d, running_total=%1.2f, " +
- "single_empty_with_acc=%s", accounts,
- accounts_with_values, amounts_with_accounts, amounts, running_total,
- (single_empty_amount && single_empty_amount_has_account) ? "true" : "false"));
-
- if (have_description && (accounts >= 2) && (accounts_with_values >= (accounts - 1)) &&
- (amounts_with_accounts == amounts) &&
- (single_empty_amount && single_empty_amount_has_account || is_zero(running_total)))
- {
- if (mSave != null) mSave.setVisible(true);
- }
- else if (mSave != null) mSave.setVisible(false);
-
- if (single_empty_amount) {
- empty_amount.setHint(String.format("%1.2f",
- (Math.abs(running_total) > 0.005) ? -running_total : 0f));
- }
-
- }
- catch (NumberFormatException e) {
- if (mSave != null) mSave.setVisible(false);
- }
- catch (Exception e) {
- e.printStackTrace();
- if (mSave != null) mSave.setVisible(false);
- }
- }
-
- @Override
- public void done(String error) {
- progress.setVisibility(View.INVISIBLE);
- Log.d("visuals", "hiding progress");
-
- if (error == null) reset_form();
- else Snackbar.make(findViewById(R.id.new_transaction_accounts_table), error,
- BaseTransientBottomBar.LENGTH_LONG).show();
-
- toggle_all_editing(true);
- check_transaction_submittable();
- }
-
- private void reset_form() {
- text_date.setText("");
- text_descr.setText("");
-
- text_descr.requestFocus();
-
- while (table.getChildCount() > 2) {
- table.removeViewAt(2);
- }
- for (int i = 0; i < 2; i++) {
- TableRow tr = (TableRow) table.getChildAt(i);
- if (tr == null) break;
-
- ((TextView) tr.getChildAt(0)).setText("");
- ((TextView) tr.getChildAt(1)).setText("");
- }
- }
-}
+++ /dev/null
-/*
- * Copyright © 2018 Damyan Ivanov.
- * This file is part of Mobile-Ledger.
- * Mobile-Ledger is free software: you can distribute it and/or modify it
- * under the term of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your opinion), any later version.
- *
- * Mobile-Ledger is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License terms for details.
- *
- * You should have received a copy of the GNU General Public License
- * along with Mobile-Ledger. If not, see <https://www.gnu.org/licenses/>.
- */
-
-package net.ktnx.mobileledger;
-
-import android.content.Context;
-import android.util.Log;
-import android.view.GestureDetector;
-import android.view.GestureDetector.SimpleOnGestureListener;
-import android.view.MotionEvent;
-import android.view.View;
-
-public abstract class OnSwipeTouchListener implements View.OnTouchListener {
- public final GestureDetector gestureDetector;
-
- OnSwipeTouchListener(Context ctx) {
- gestureDetector = new GestureDetector(ctx, new GestureListener() );
- }
-
- private final class GestureListener extends SimpleOnGestureListener {
- private static final int SWIPE_THRESHOLD = 100;
- private static final int SWIPE_VELOCITY_THRESHOLD = 100;
-
- @Override
- public boolean onDown(MotionEvent e) {
- Log.d("sw-l", "onDown");
- return false;
- }
-
- @Override
- public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
- boolean result = false;
-
- Log.d("sw-l", "onFling");
-
- try {
- float diffX = e2.getX() - e1.getX();
- float diffY = e2.getY() - e1.getY();
- if (Math.abs(diffX) > Math.abs(diffY)) {
- if (Math.abs(diffX) > SWIPE_THRESHOLD && Math.abs(velocityX) > SWIPE_VELOCITY_THRESHOLD) {
- if (diffX > 0) {
- Log.d("sw-l", "calling onSwipeRight");
- onSwipeRight();
- }
- else {
- Log.d("sw-l", "calling onSwipeLeft");
- onSwipeLeft();
- }
- }
- result = true;
- }
- else if (Math.abs(diffY) > SWIPE_THRESHOLD && Math.abs(velocityY) > SWIPE_VELOCITY_THRESHOLD) {
- if (diffY > 0) {
- onSwipeDown();
- }
- else {
- onSwipeUp();
- }
- result = true;
- }
- }
- catch (Exception e) {
- e.printStackTrace();
- }
-
- return result;
- }
- }
-
- public void onSwipeRight() {}
- public void onSwipeLeft() {
- Log.d("sw-l", "LEFT");
- }
- public void onSwipeUp() {}
- public void onSwipeDown() {}
-}
+++ /dev/null
-/*
- * Copyright © 2018 Damyan Ivanov.
- * This file is part of Mobile-Ledger.
- * Mobile-Ledger is free software: you can distribute it and/or modify it
- * under the term of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your opinion), any later version.
- *
- * Mobile-Ledger is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License terms for details.
- *
- * You should have received a copy of the GNU General Public License
- * along with Mobile-Ledger. If not, see <https://www.gnu.org/licenses/>.
- */
-
-package net.ktnx.mobileledger;
-
-import android.content.Context;
-import android.support.annotation.NonNull;
-import android.support.v7.widget.RecyclerView;
-import android.support.v7.widget.RecyclerView.OnItemTouchListener;
-import android.view.GestureDetector;
-import android.view.MotionEvent;
-import android.view.View;
-
-class RecyclerItemListener implements OnItemTouchListener {
- private RecyclerTouchListener listener;
- private GestureDetector gd;
-
- interface RecyclerTouchListener {
- void onClickItem(View v, int position);
- void onLongClickItem(View v, int position);
- }
-
- public RecyclerItemListener(Context ctx, RecyclerView rv, RecyclerTouchListener listener) {
- this.listener = listener;
- this.gd = new GestureDetector(
- ctx, new GestureDetector.SimpleOnGestureListener() {
- @Override
- public void onLongPress(MotionEvent e) {
- View v = rv.findChildViewUnder(e.getX(), e.getY());
- listener.onLongClickItem(v, rv.getChildAdapterPosition(v));
- }
-
- @Override
- public boolean onSingleTapUp(MotionEvent e) {
- View v = rv.findChildViewUnder(e.getX(), e.getY());
- listener.onClickItem(v, rv.getChildAdapterPosition(v));
- return true;
- }
- }
- );
- }
-
- @Override
- public boolean onInterceptTouchEvent(@NonNull RecyclerView recyclerView,
- @NonNull MotionEvent motionEvent) {
- View v = recyclerView.findChildViewUnder(motionEvent.getX(), motionEvent.getY());
- return (v != null) && gd.onTouchEvent(motionEvent);
- }
-
- @Override
- public void onTouchEvent(@NonNull RecyclerView recyclerView, @NonNull MotionEvent motionEvent) {
-
- }
-
- @Override
- public void onRequestDisallowInterceptTouchEvent(boolean b) {
-
- }
-}
+++ /dev/null
-/*
- * Copyright © 2018 Damyan Ivanov.
- * This file is part of Mobile-Ledger.
- * Mobile-Ledger is free software: you can distribute it and/or modify it
- * under the term of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your opinion), any later version.
- *
- * Mobile-Ledger is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License terms for details.
- *
- * You should have received a copy of the GNU General Public License
- * along with Mobile-Ledger. If not, see <https://www.gnu.org/licenses/>.
- */
-
-package net.ktnx.mobileledger;
-
-import android.annotation.TargetApi;
-import android.content.Context;
-import android.content.Intent;
-import android.content.res.Configuration;
-import android.media.Ringtone;
-import android.media.RingtoneManager;
-import android.net.Uri;
-import android.os.Build;
-import android.os.Bundle;
-import android.preference.ListPreference;
-import android.preference.Preference;
-import android.preference.PreferenceActivity;
-import android.preference.PreferenceFragment;
-import android.preference.PreferenceManager;
-import android.preference.RingtonePreference;
-import android.support.v4.app.NavUtils;
-import android.support.v7.app.ActionBar;
-import android.text.TextUtils;
-import android.view.MenuItem;
-
-import java.util.List;
-
-/**
- * A {@link PreferenceActivity} that presents a set of application settings. On
- * handset devices, settings are presented as a single list. On tablets,
- * settings are split by category, with category headers shown to the left of
- * the list of settings.
- * <p>
- * See <a href="http://developer.android.com/design/patterns/settings.html">
- * Android Design: Settings</a> for design guidelines and the <a
- * href="http://developer.android.com/guide/topics/ui/settings.html">Settings
- * API Guide</a> for more information on developing a Settings UI.
- */
-public class SettingsActivity extends AppCompatPreferenceActivity {
- public static String PREF_KEY_SHOW_ONLY_STARRED_ACCOUNTS = "pref_show_only_starred_accounts";
-
- /**
- * A preference value change listener that updates the preference's summary
- * to reflect its new value.
- */
- private static Preference.OnPreferenceChangeListener sBindPreferenceSummaryToValueListener = (preference, value) -> {
- String stringValue = value.toString();
-
- AccountSummary.preferences_changed();
-
- if (preference instanceof ListPreference) {
- // For list preferences, look up the correct display value in
- // the preference's 'entries' list.
- ListPreference listPreference = (ListPreference) preference;
- int index = listPreference.findIndexOfValue(stringValue);
-
- // Set the summary to reflect the new value.
- preference.setSummary(
- index >= 0
- ? listPreference.getEntries()[index]
- : null);
-
- } else if (preference instanceof RingtonePreference) {
- // For ringtone preferences, look up the correct display value
- // using RingtoneManager.
- if (TextUtils.isEmpty(stringValue)) {
- // Empty values correspond to 'silent' (no ringtone).
- preference.setSummary(R.string.pref_ringtone_silent);
-
- } else {
- Ringtone ringtone = RingtoneManager.getRingtone(
- preference.getContext(), Uri.parse(stringValue));
-
- if (ringtone == null) {
- // Clear the summary if there was a lookup error.
- preference.setSummary(null);
- } else {
- // Set the summary to reflect the new ringtone display
- // name.
- String name = ringtone.getTitle(preference.getContext());
- preference.setSummary(name);
- }
- }
- } else {
- // For all other preferences, set the summary to the value's
- // simple string representation.
- preference.setSummary(stringValue);
- }
-
- return true;
- };
-
- /**
- * Helper method to determine if the device has an extra-large screen. For
- * example, 10" tablets are extra-large.
- */
- private static boolean isXLargeTablet(Context context) {
- return (context.getResources().getConfiguration().screenLayout
- & Configuration.SCREENLAYOUT_SIZE_MASK) >= Configuration.SCREENLAYOUT_SIZE_XLARGE;
- }
-
- /**
- * Binds a preference's summary to its value. More specifically, when the
- * preference's value is changed, its summary (line of text below the
- * preference title) is updated to reflect the value. The summary is also
- * immediately updated upon calling this method. The exact display format is
- * dependent on the type of preference.
- *
- * @see #sBindPreferenceSummaryToValueListener
- */
- private static void bindPreferenceSummaryToValue(Preference preference) {
- // Set the listener to watch for value changes.
- preference.setOnPreferenceChangeListener(sBindPreferenceSummaryToValueListener);
-
- // Trigger the listener immediately with the preference's
- // current value.
- sBindPreferenceSummaryToValueListener.onPreferenceChange(preference,
- PreferenceManager
- .getDefaultSharedPreferences(preference.getContext())
- .getString(preference.getKey(), ""));
- }
-
- @Override
- protected void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- setupActionBar();
- }
-
- /**
- * Set up the {@link android.app.ActionBar}, if the API is available.
- */
- private void setupActionBar() {
- ActionBar actionBar = getSupportActionBar();
- if (actionBar != null) {
- // Show the Up button in the action bar.
- actionBar.setDisplayHomeAsUpEnabled(true);
- }
- }
-
- @Override
- public boolean onMenuItemSelected(int featureId, MenuItem item) {
- int id = item.getItemId();
- if (id == android.R.id.home) {
- if (!super.onMenuItemSelected(featureId, item)) {
- NavUtils.navigateUpFromSameTask(this);
- }
- return true;
- }
- return super.onMenuItemSelected(featureId, item);
- }
-
- /**
- * {@inheritDoc}
- */
- @Override
- public boolean onIsMultiPane() {
- return isXLargeTablet(this);
- }
-
- /**
- * {@inheritDoc}
- */
- @Override
- @TargetApi(Build.VERSION_CODES.HONEYCOMB)
- public void onBuildHeaders(List<Header> target) {
- loadHeadersFromResource(R.xml.pref_headers, target);
- }
-
- /**
- * This method stops fragment injection in malicious applications.
- * Make sure to deny any unknown fragments here.
- */
- protected boolean isValidFragment(String fragmentName) {
- return PreferenceFragment.class.getName().equals(fragmentName)
- || BackendPreferenceFragment.class.getName().equals(fragmentName)
- || DataSyncPreferenceFragment.class.getName().equals(fragmentName)
- || NotificationPreferenceFragment.class.getName().equals(fragmentName)
- || InterfacePreferenceFragment.class.getName().equals(fragmentName);
- }
-
- /**
- * This fragment shows general preferences only. It is used when the
- * activity is showing a two-pane settings UI.
- */
- @TargetApi(Build.VERSION_CODES.HONEYCOMB)
- public static class BackendPreferenceFragment extends PreferenceFragment {
- @Override
- public void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- addPreferencesFromResource(R.xml.pref_backend);
- setHasOptionsMenu(true);
-
- // Bind the summaries of EditText/List/Dialog/Ringtone preferences
- // to their values. When their values change, their summaries are
- // updated to reflect the new value, per the Android Design
- // guidelines.
- bindPreferenceSummaryToValue(findPreference("backend_url"));
- bindPreferenceSummaryToValue(findPreference("backend_auth_user"));
-
- }
-
- @Override
- public boolean onOptionsItemSelected(MenuItem item) {
- int id = item.getItemId();
- if (id == android.R.id.home) {
- startActivity(new Intent(getActivity(), SettingsActivity.class));
- return true;
- }
- return super.onOptionsItemSelected(item);
- }
- }
-
- /**
- * This fragment shows general preferences only. It is used when the
- * activity is showing a two-pane settings UI.
- */
- @TargetApi(Build.VERSION_CODES.HONEYCOMB)
- public static
- class InterfacePreferenceFragment extends PreferenceFragment {
- @Override
- public
- void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- addPreferencesFromResource(R.xml.pref_interface);
- setHasOptionsMenu(true);
-
- // Bind the summaries of EditText/List/Dialog/Ringtone preferences
- // to their values. When their values change, their summaries are
- // updated to reflect the new value, per the Android Design
- // guidelines.
-
- }
-
- @Override
- public
- boolean onOptionsItemSelected(MenuItem item) {
- int id = item.getItemId();
- if (id == android.R.id.home) {
- startActivity(new Intent(getActivity(), SettingsActivity.class));
- return true;
- }
- return super.onOptionsItemSelected(item);
- }
- }
-
- /**
- * This fragment shows notification preferences only. It is used when the
- * activity is showing a two-pane settings UI.
- */
- @TargetApi(Build.VERSION_CODES.HONEYCOMB)
- public static class NotificationPreferenceFragment extends PreferenceFragment {
- @Override
- public void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- addPreferencesFromResource(R.xml.pref_notification);
- setHasOptionsMenu(true);
-
- // Bind the summaries of EditText/List/Dialog/Ringtone preferences
- // to their values. When their values change, their summaries are
- // updated to reflect the new value, per the Android Design
- // guidelines.
- bindPreferenceSummaryToValue(findPreference("notifications_new_message_ringtone"));
- }
-
- @Override
- public boolean onOptionsItemSelected(MenuItem item) {
- int id = item.getItemId();
- if (id == android.R.id.home) {
- startActivity(new Intent(getActivity(), SettingsActivity.class));
- return true;
- }
- return super.onOptionsItemSelected(item);
- }
- }
-
- /**
- * This fragment shows data and sync preferences only. It is used when the
- * activity is showing a two-pane settings UI.
- */
- @TargetApi(Build.VERSION_CODES.HONEYCOMB)
- public static class DataSyncPreferenceFragment extends PreferenceFragment {
- @Override
- public void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- addPreferencesFromResource(R.xml.pref_data_sync);
- setHasOptionsMenu(true);
-
- // Bind the summaries of EditText/List/Dialog/Ringtone preferences
- // to their values. When their values change, their summaries are
- // updated to reflect the new value, per the Android Design
- // guidelines.
- bindPreferenceSummaryToValue(findPreference("sync_frequency"));
- }
-
- @Override
- public boolean onOptionsItemSelected(MenuItem item) {
- int id = item.getItemId();
- if (id == android.R.id.home) {
- startActivity(new Intent(getActivity(), SettingsActivity.class));
- return true;
- }
- return super.onOptionsItemSelected(item);
- }
- }
-}
+++ /dev/null
-/*
- * Copyright © 2018 Damyan Ivanov.
- * This file is part of Mobile-Ledger.
- * Mobile-Ledger is free software: you can distribute it and/or modify it
- * under the term of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your opinion), any later version.
- *
- * Mobile-Ledger is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License terms for details.
- *
- * You should have received a copy of the GNU General Public License
- * along with Mobile-Ledger. If not, see <https://www.gnu.org/licenses/>.
- */
-
-package net.ktnx.mobileledger;
-
-import android.arch.lifecycle.ViewModelProviders;
-import android.database.MatrixCursor;
-import android.os.Build;
-import android.os.Bundle;
-import android.preference.PreferenceManager;
-import android.support.v4.widget.SwipeRefreshLayout;
-import android.support.v7.app.ActionBar;
-import android.support.v7.app.AppCompatActivity;
-import android.support.v7.widget.LinearLayoutManager;
-import android.support.v7.widget.RecyclerView;
-import android.support.v7.widget.Toolbar;
-import android.util.Log;
-import android.view.Menu;
-import android.view.MenuItem;
-import android.view.View;
-import android.view.inputmethod.InputMethodManager;
-import android.widget.AdapterView;
-import android.widget.AutoCompleteTextView;
-import android.widget.LinearLayout;
-import android.widget.ProgressBar;
-import android.widget.TextView;
-
-import net.ktnx.mobileledger.async.RetrieveTransactionsTask;
-import net.ktnx.mobileledger.ui.transaction_list.TransactionListViewModel;
-import net.ktnx.mobileledger.utils.MLDB;
-
-import java.lang.ref.WeakReference;
-import java.time.ZoneId;
-import java.time.format.DateTimeFormatter;
-import java.util.Date;
-
-public class TransactionListActivity extends AppCompatActivity {
- public TransactionListViewModel model;
- private View bTransactionListCancelDownload;
- private MenuItem menuTransactionListFilter;
- private View vAccountFilter;
- private SwipeRefreshLayout swiper;
- private RecyclerView root;
- private ProgressBar progressBar;
- private LinearLayout progressLayout;
- private TextView tvLastUpdate;
- private TransactionListAdapter modelAdapter;
- private RetrieveTransactionsTask retrieveTransactionsTask;
- private AutoCompleteTextView accNameFilter;
- @Override
- protected void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- setContentView(R.layout.transaction_list_activity);
-
- Toolbar toolbar = findViewById(R.id.toolbar);
- setSupportActionBar(toolbar);
-
- setupActionBar();
-
- swiper = findViewById(R.id.transaction_swipe);
- if (swiper == null) throw new RuntimeException("Can't get hold on the swipe layout");
- root = findViewById(R.id.transaction_root);
- if (root == null) throw new RuntimeException("Can't get hold on the transaction list view");
- progressBar = findViewById(R.id.transaction_list_progress_bar);
- if (progressBar == null)
- throw new RuntimeException("Can't get hold on the transaction list progress bar");
- progressLayout = findViewById(R.id.transaction_progress_layout);
- if (progressLayout == null) throw new RuntimeException(
- "Can't get hold on the transaction list progress bar layout");
- tvLastUpdate = findViewById(R.id.transactions_last_update);
- updateLastUpdateText();
- model = ViewModelProviders.of(this).get(TransactionListViewModel.class);
- modelAdapter = new TransactionListAdapter(model);
-
- RecyclerView root = findViewById(R.id.transaction_root);
- root.setAdapter(modelAdapter);
-
- LinearLayoutManager llm = new LinearLayoutManager(this);
-
- llm.setOrientation(LinearLayoutManager.VERTICAL);
- root.setLayoutManager(llm);
-
- swiper.setOnRefreshListener(() -> {
- Log.d("ui", "refreshing transactions via swipe");
- update_transactions();
- });
-
- swiper.setColorSchemeResources(R.color.colorPrimary, R.color.colorAccent);
-
- vAccountFilter = findViewById(R.id.transaction_list_account_name_filter);
- accNameFilter = findViewById(R.id.transaction_filter_account_name);
- bTransactionListCancelDownload = findViewById(R.id.transaction_list_cancel_download);
-
- MLDB.hook_autocompletion_adapter(this, accNameFilter, "accounts", "name");
- TransactionListActivity me = this;
- accNameFilter.setOnItemClickListener(new AdapterView.OnItemClickListener() {
- @Override
- public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
- Log.d("tmp", "direct onItemClick");
- model.reloadTransactions(me);
- MatrixCursor mc = (MatrixCursor) parent.getItemAtPosition(position);
- modelAdapter.setBoldAccountName(mc.getString(1));
- modelAdapter.notifyDataSetChanged();
- me.hideSoftKeyboard();
- }
- });
-
- updateLastUpdateText();
- long last_update = MLDB.get_option_value(this, MLDB.OPT_TRANSACTION_LIST_STAMP, 0L);
- Log.d("transactions", String.format("Last update = %d", last_update));
- if (last_update == 0) {
- update_transactions();
- }
- else {
- model.reloadTransactions(this);
- }
- }
- private void setupActionBar() {
- ActionBar actionBar = getSupportActionBar();
- if (actionBar != null) {
- // Show the Up button in the action bar.
- actionBar.setDisplayHomeAsUpEnabled(true);
- }
- }
- @Override
- public void finish() {
- super.finish();
- Log.d("visuals", "finishing");
- overridePendingTransition(R.anim.dummy, R.anim.slide_out_right);
- }
- private void update_transactions() {
- retrieveTransactionsTask = new RetrieveTransactionsTask(new WeakReference<>(this));
-
- RetrieveTransactionsTask.Params params = new RetrieveTransactionsTask.Params(
- PreferenceManager.getDefaultSharedPreferences(this));
-
- retrieveTransactionsTask.execute(params);
- bTransactionListCancelDownload.setEnabled(true);
- }
-
- public void onRetrieveStart() {
- progressBar.setIndeterminate(true);
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) progressBar.setProgress(0, false);
- else progressBar.setProgress(0);
- progressLayout.setVisibility(View.VISIBLE);
- }
- @Override
- public boolean onCreateOptionsMenu(Menu menu) {
- getMenuInflater().inflate(R.menu.transaction_list, menu);
- menuTransactionListFilter = menu.findItem(R.id.menu_transaction_list_filter);
- if ((menuTransactionListFilter == null)) throw new AssertionError();
-
- return true;
- }
- public void onRetrieveProgress(RetrieveTransactionsTask.Progress progress) {
- if ((progress.getTotal() == RetrieveTransactionsTask.Progress.INDETERMINATE) ||
- (progress.getTotal() == 0))
- {
- progressBar.setIndeterminate(true);
- }
- else {
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
- progressBar.setMin(0);
- }
- progressBar.setMax(progress.getTotal());
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
- progressBar.setProgress(progress.getProgress(), true);
- }
- else progressBar.setProgress(progress.getProgress());
- progressBar.setIndeterminate(false);
- }
- }
-
- public void onRetrieveDone(boolean success) {
- progressLayout.setVisibility(View.GONE);
- swiper.setRefreshing(false);
- updateLastUpdateText();
- if (success) {
- Log.d("transactions", "calling notifyDataSetChanged()");
- modelAdapter.notifyDataSetChanged();
- }
- }
- private void updateLastUpdateText() {
- {
- long last_update = MLDB.get_option_value(this, MLDB.OPT_TRANSACTION_LIST_STAMP, 0L);
- Log.d("transactions", String.format("Last update = %d", last_update));
- if (last_update == 0) {
- tvLastUpdate.setText(getString(R.string.transaction_last_update_never));
- }
- else {
- Date date = new Date(last_update);
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
- tvLastUpdate.setText(date.toInstant().atZone(ZoneId.systemDefault())
- .format(DateTimeFormatter.ISO_LOCAL_DATE_TIME));
- }
- else {
- tvLastUpdate.setText(date.toLocaleString());
- }
- }
- }
- }
- public void onClearAccountNameClick(View view) {
- vAccountFilter.setVisibility(View.GONE);
- menuTransactionListFilter.setVisible(true);
- accNameFilter.setText(null);
- model.reloadTransactions(this);
- modelAdapter.resetBoldAccountName();
- modelAdapter.notifyDataSetChanged();
- hideSoftKeyboard();
- }
- private void hideSoftKeyboard() {
- // hide the keyboard
- View v = getCurrentFocus();
- if (v != null) {
- InputMethodManager imm = (InputMethodManager) getSystemService(INPUT_METHOD_SERVICE);
- imm.hideSoftInputFromWindow(v.getWindowToken(), 0);
- }
- }
- public void onShowFilterClick(MenuItem menuItem) {
- vAccountFilter.setVisibility(View.VISIBLE);
- menuTransactionListFilter.setVisible(false);
- accNameFilter.requestFocus();
- InputMethodManager imm = (InputMethodManager) getSystemService(INPUT_METHOD_SERVICE);
- imm.showSoftInput(accNameFilter, 0);
- }
- public void onStopTransactionRefreshClick(View view) {
- Log.d("interactive", "Cancelling transactions refresh");
- if (retrieveTransactionsTask != null) retrieveTransactionsTask.cancel(false);
- bTransactionListCancelDownload.setEnabled(false);
- }
-}
+++ /dev/null
-/*
- * Copyright © 2018 Damyan Ivanov.
- * This file is part of Mobile-Ledger.
- * Mobile-Ledger is free software: you can distribute it and/or modify it
- * under the term of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your opinion), any later version.
- *
- * Mobile-Ledger is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License terms for details.
- *
- * You should have received a copy of the GNU General Public License
- * along with Mobile-Ledger. If not, see <https://www.gnu.org/licenses/>.
- */
-
-package net.ktnx.mobileledger;
-
-import android.content.Context;
-import android.database.sqlite.SQLiteDatabase;
-import android.graphics.Typeface;
-import android.support.annotation.NonNull;
-import android.support.constraint.ConstraintLayout;
-import android.support.v7.widget.AppCompatTextView;
-import android.support.v7.widget.RecyclerView;
-import android.util.Log;
-import android.view.Gravity;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.LinearLayout;
-import android.widget.TextView;
-
-import net.ktnx.mobileledger.model.LedgerTransaction;
-import net.ktnx.mobileledger.model.LedgerTransactionAccount;
-import net.ktnx.mobileledger.ui.transaction_list.TransactionListViewModel;
-import net.ktnx.mobileledger.utils.Globals;
-import net.ktnx.mobileledger.utils.MLDB;
-
-import static net.ktnx.mobileledger.utils.DimensionUtils.dp2px;
-
-class TransactionListAdapter
- extends RecyclerView.Adapter<TransactionListAdapter.TransactionRowHolder> {
- TransactionListViewModel model;
- private String boldAccountName;
- public TransactionListAdapter(TransactionListViewModel model) {
- this.model = model;
- }
- public void onBindViewHolder(@NonNull TransactionRowHolder holder, int position) {
- LedgerTransaction tr = model.getTransaction(position);
- // in a race when transaction list is reduced, but the model hasn't been notified yet
- // the view will disappear when the notifications reaches the model, so by simply omitting
- // the out-of-range get() call nothing bad happens - just a to-be-deleted view remains
- // a bit longer
- if (tr == null) return;
-
- Context ctx = holder.row.getContext();
-
- try (SQLiteDatabase db = MLDB.getReadableDatabase(ctx)) {
- tr.loadData(db);
- holder.tvDescription.setText(tr.getDescription());
- holder.tvDate.setText(tr.getDate());
-
- int rowIndex = 0;
- for (LedgerTransactionAccount acc : tr.getAccounts()) {
- LinearLayout row = (LinearLayout) holder.tableAccounts.getChildAt(rowIndex++);
- TextView accName, accAmount;
- if (row == null) {
- row = new LinearLayout(ctx);
- row.setLayoutParams(
- new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT,
- LinearLayout.LayoutParams.WRAP_CONTENT));
- row.setGravity(Gravity.CENTER_VERTICAL);
- row.setOrientation(LinearLayout.HORIZONTAL);
- row.setPaddingRelative(dp2px(ctx, 8), 0, 0, 0);
- accName = new AppCompatTextView(ctx);
- accName.setLayoutParams(
- new LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT,
- 5f));
- accName.setTextAlignment(View.TEXT_ALIGNMENT_VIEW_START);
- row.addView(accName);
- accAmount = new AppCompatTextView(ctx);
- LinearLayout.LayoutParams llp =
- new LinearLayout.LayoutParams(LinearLayout.LayoutParams.WRAP_CONTENT,
- LinearLayout.LayoutParams.WRAP_CONTENT);
- llp.setMarginEnd(0);
- accAmount.setLayoutParams(llp);
- accAmount.setTextAlignment(View.TEXT_ALIGNMENT_VIEW_END);
- accAmount.setMinWidth(dp2px(ctx, 60));
- row.addView(accAmount);
- holder.tableAccounts.addView(row);
- }
- else {
- accName = (TextView) row.getChildAt(0);
- accAmount = (TextView) row.getChildAt(1);
- }
- accName.setText(acc.getAccountName());
- accAmount.setText(acc.toString());
-
- if ((boldAccountName != null) && boldAccountName.equals(acc.getAccountName())) {
- accName.setTypeface(null, Typeface.BOLD);
- accAmount.setTypeface(null, Typeface.BOLD);
- accName.setTextColor(Globals.primaryDark);
- accAmount.setTextColor(Globals.primaryDark);
- }
- else {
- accName.setTypeface(null, Typeface.NORMAL);
- accAmount.setTypeface(null, Typeface.NORMAL);
- accName.setTextColor(Globals.defaultTextColor);
- accAmount.setTextColor(Globals.defaultTextColor);
- }
-
- }
- if (holder.tableAccounts.getChildCount() > rowIndex) {
- holder.tableAccounts
- .removeViews(rowIndex, holder.tableAccounts.getChildCount() - rowIndex);
- }
-
- if (position % 2 == 0) {
- holder.row.setBackgroundColor(Globals.table_row_even_bg);
- }
- else {
- holder.row.setBackgroundColor(Globals.table_row_odd_bg);
- }
-
- Log.d("transactions", String.format("Filled position %d", position));
- }
- }
-
- @NonNull
- @Override
- public TransactionRowHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
- Log.d("perf", "onCreateViewHolder called");
- View row = LayoutInflater.from(parent.getContext())
- .inflate(R.layout.transaction_list_row, parent, false);
- return new TransactionRowHolder(row);
- }
-
- @Override
- public int getItemCount() {
- return model.getTransactionCount();
- }
- public void setBoldAccountName(String boldAccountName) {
- this.boldAccountName = boldAccountName;
- }
- public void resetBoldAccountName() {
- this.boldAccountName = null;
- }
- class TransactionRowHolder extends RecyclerView.ViewHolder {
- TextView tvDescription, tvDate;
- LinearLayout tableAccounts;
- ConstraintLayout row;
- public TransactionRowHolder(@NonNull View itemView) {
- super(itemView);
- this.row = itemView.findViewById(R.id.transaction_row);
- this.tvDescription = itemView.findViewById(R.id.transaction_row_description);
- this.tvDate = itemView.findViewById(R.id.transaction_row_date);
- this.tableAccounts = itemView.findViewById(R.id.transaction_row_acc_amounts);
- }
- }
-}
\ No newline at end of file
import android.database.sqlite.SQLiteDatabase;
import android.util.Log;
-import net.ktnx.mobileledger.AccountSummary;
+import net.ktnx.mobileledger.ui.account_summary.AccountSummaryFragment;
import net.ktnx.mobileledger.R;
import net.ktnx.mobileledger.model.LedgerAccount;
import net.ktnx.mobileledger.utils.MLDB;
public class RetrieveAccountsTask extends android.os.AsyncTask<Void, Integer, Void> {
int error;
- WeakReference<AccountSummary> mContext;
+ WeakReference<AccountSummaryFragment> mContext;
private SharedPreferences pref;
- public RetrieveAccountsTask(WeakReference<AccountSummary> context) {
+ public RetrieveAccountsTask(WeakReference<AccountSummaryFragment> context) {
mContext = context;
error = 0;
}
try {
HttpURLConnection http = NetworkUtil.prepare_connection(pref, "add");
publishProgress(0);
- try (SQLiteDatabase db = MLDB.getWritableDatabase(mContext.get())) {
+ try (SQLiteDatabase db = MLDB.getWritableDatabase(mContext.get().getActivity())) {
try (InputStream resp = http.getInputStream()) {
Log.d("update_accounts", String.valueOf(http.getResponseCode()));
if (http.getResponseCode() != 200) {
}
@Override
protected void onPostExecute(Void result) {
- AccountSummary ctx = mContext.get();
+ AccountSummaryFragment ctx = mContext.get();
if (ctx == null) return;
ctx.onAccountRefreshDone(this.error);
}
import android.util.Log;
import net.ktnx.mobileledger.R;
-import net.ktnx.mobileledger.TransactionListActivity;
import net.ktnx.mobileledger.model.LedgerTransaction;
import net.ktnx.mobileledger.model.LedgerTransactionAccount;
+import net.ktnx.mobileledger.ui.transaction_list.TransactionListFragment;
import net.ktnx.mobileledger.utils.MLDB;
import net.ktnx.mobileledger.utils.NetworkUtil;
private static final Pattern transactionDetailsPattern =
Pattern.compile("^\\s+" + "(\\S[\\S\\s]+\\S)\\s\\s+([-+]?\\d[\\d,.]*)(?:\\s+(\\S+)$)?");
private static final Pattern endPattern = Pattern.compile("\\bid=\"addmodal\"");
- protected WeakReference<TransactionListActivity> contextRef;
+ protected WeakReference<TransactionListFragment> contextRef;
protected int error;
private boolean success;
- public RetrieveTransactionsTask(WeakReference<TransactionListActivity> contextRef) {
+ public RetrieveTransactionsTask(WeakReference<TransactionListFragment> contextRef) {
this.contextRef = contextRef;
}
private static final void L(String msg) {
@Override
protected void onProgressUpdate(Progress... values) {
super.onProgressUpdate(values);
- TransactionListActivity context = getContext();
+ TransactionListFragment context = getContext();
if (context == null) return;
context.onRetrieveProgress(values[0]);
}
@Override
protected void onPreExecute() {
super.onPreExecute();
- TransactionListActivity context = getContext();
+ TransactionListFragment context = getContext();
if (context == null) return;
context.onRetrieveStart();
}
@Override
protected void onPostExecute(Void aVoid) {
super.onPostExecute(aVoid);
- TransactionListActivity context = getContext();
+ TransactionListFragment context = getContext();
if (context == null) return;
context.onRetrieveDone(success);
}
@Override
protected void onCancelled() {
super.onCancelled();
- TransactionListActivity context = getContext();
+ TransactionListFragment context = getContext();
if (context == null) return;
context.onRetrieveDone(false);
}
NetworkUtil.prepare_connection(params[0].getBackendPref(), "journal");
http.setAllowUserInteraction(false);
publishProgress(progress);
- TransactionListActivity ctx = contextRef.get();
+ TransactionListFragment ctx = getContext();
if (ctx == null) return null;
- try (SQLiteDatabase db = MLDB.getWritableDatabase(ctx)) {
+ try (SQLiteDatabase db = MLDB.getWritableDatabase(ctx.getActivity())) {
try (InputStream resp = http.getInputStream()) {
if (http.getResponseCode() != 200) throw new IOException(
String.format("HTTP error %d", http.getResponseCode()));
if (success && !isCancelled()) {
Log.d("db", "Updating transaction list stamp");
- MLDB.set_option_value(ctx, MLDB.OPT_TRANSACTION_LIST_STAMP, new Date().getTime());
+ MLDB.set_option_value(ctx.getActivity(), MLDB.OPT_TRANSACTION_LIST_STAMP,
+ new Date().getTime());
ctx.model.reloadTransactions(ctx);
}
}
}
return null;
}
- TransactionListActivity getContext() {
+ TransactionListFragment getContext() {
return contextRef.get();
}
--- /dev/null
+/*
+ * Copyright © 2018 Damyan Ivanov.
+ * This file is part of Mobile-Ledger.
+ * Mobile-Ledger is free software: you can distribute it and/or modify it
+ * under the term of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your opinion), any later version.
+ *
+ * Mobile-Ledger is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License terms for details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Mobile-Ledger. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package net.ktnx.mobileledger.ui;
+
+import android.content.Context;
+import android.util.Log;
+import android.view.GestureDetector;
+import android.view.GestureDetector.SimpleOnGestureListener;
+import android.view.MotionEvent;
+import android.view.View;
+
+public abstract class OnSwipeTouchListener implements View.OnTouchListener {
+ public final GestureDetector gestureDetector;
+
+ protected OnSwipeTouchListener(Context ctx) {
+ gestureDetector = new GestureDetector(ctx, new GestureListener() );
+ }
+
+ private final class GestureListener extends SimpleOnGestureListener {
+ private static final int SWIPE_THRESHOLD = 100;
+ private static final int SWIPE_VELOCITY_THRESHOLD = 100;
+
+ @Override
+ public boolean onDown(MotionEvent e) {
+ Log.d("sw-l", "onDown");
+ return false;
+ }
+
+ @Override
+ public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
+ boolean result = false;
+
+ Log.d("sw-l", "onFling");
+
+ try {
+ float diffX = e2.getX() - e1.getX();
+ float diffY = e2.getY() - e1.getY();
+ if (Math.abs(diffX) > Math.abs(diffY)) {
+ if (Math.abs(diffX) > SWIPE_THRESHOLD && Math.abs(velocityX) > SWIPE_VELOCITY_THRESHOLD) {
+ if (diffX > 0) {
+ Log.d("sw-l", "calling onSwipeRight");
+ onSwipeRight();
+ }
+ else {
+ Log.d("sw-l", "calling onSwipeLeft");
+ onSwipeLeft();
+ }
+ }
+ result = true;
+ }
+ else if (Math.abs(diffY) > SWIPE_THRESHOLD && Math.abs(velocityY) > SWIPE_VELOCITY_THRESHOLD) {
+ if (diffY > 0) {
+ onSwipeDown();
+ }
+ else {
+ onSwipeUp();
+ }
+ result = true;
+ }
+ }
+ catch (Exception e) {
+ e.printStackTrace();
+ }
+
+ return result;
+ }
+ }
+
+ public void onSwipeRight() {}
+ public void onSwipeLeft() {
+ Log.d("sw-l", "LEFT");
+ }
+ public void onSwipeUp() {}
+ public void onSwipeDown() {}
+}
--- /dev/null
+/*
+ * Copyright © 2018 Damyan Ivanov.
+ * This file is part of Mobile-Ledger.
+ * Mobile-Ledger is free software: you can distribute it and/or modify it
+ * under the term of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your opinion), any later version.
+ *
+ * Mobile-Ledger is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License terms for details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Mobile-Ledger. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package net.ktnx.mobileledger.ui;
+
+import android.content.Context;
+import android.support.annotation.NonNull;
+import android.support.v7.widget.RecyclerView;
+import android.support.v7.widget.RecyclerView.OnItemTouchListener;
+import android.view.GestureDetector;
+import android.view.MotionEvent;
+import android.view.View;
+
+public class RecyclerItemListener implements OnItemTouchListener {
+ private RecyclerTouchListener listener;
+ private GestureDetector gd;
+
+ public interface RecyclerTouchListener {
+ void onClickItem(View v, int position);
+ void onLongClickItem(View v, int position);
+ }
+
+ public RecyclerItemListener(Context ctx, RecyclerView rv, RecyclerTouchListener listener) {
+ this.listener = listener;
+ this.gd = new GestureDetector(
+ ctx, new GestureDetector.SimpleOnGestureListener() {
+ @Override
+ public void onLongPress(MotionEvent e) {
+ View v = rv.findChildViewUnder(e.getX(), e.getY());
+ listener.onLongClickItem(v, rv.getChildAdapterPosition(v));
+ }
+
+ @Override
+ public boolean onSingleTapUp(MotionEvent e) {
+ View v = rv.findChildViewUnder(e.getX(), e.getY());
+ listener.onClickItem(v, rv.getChildAdapterPosition(v));
+ return true;
+ }
+ }
+ );
+ }
+
+ @Override
+ public boolean onInterceptTouchEvent(@NonNull RecyclerView recyclerView,
+ @NonNull MotionEvent motionEvent) {
+ View v = recyclerView.findChildViewUnder(motionEvent.getX(), motionEvent.getY());
+ return (v != null) && gd.onTouchEvent(motionEvent);
+ }
+
+ @Override
+ public void onTouchEvent(@NonNull RecyclerView recyclerView, @NonNull MotionEvent motionEvent) {
+
+ }
+
+ @Override
+ public void onRequestDisallowInterceptTouchEvent(boolean b) {
+
+ }
+}
--- /dev/null
+/*
+ * Copyright © 2018 Damyan Ivanov.
+ * This file is part of Mobile-Ledger.
+ * Mobile-Ledger is free software: you can distribute it and/or modify it
+ * under the term of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your opinion), any later version.
+ *
+ * Mobile-Ledger is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License terms for details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Mobile-Ledger. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package net.ktnx.mobileledger.ui.account_summary;
+
+import android.arch.lifecycle.ViewModelProviders;
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.os.Bundle;
+import android.preference.PreferenceManager;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.support.design.widget.FloatingActionButton;
+import android.support.design.widget.Snackbar;
+import android.support.v4.app.Fragment;
+import android.support.v4.widget.SwipeRefreshLayout;
+import android.support.v7.widget.LinearLayoutManager;
+import android.support.v7.widget.RecyclerView;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.ViewGroup;
+
+import net.ktnx.mobileledger.R;
+import net.ktnx.mobileledger.ui.RecyclerItemListener;
+import net.ktnx.mobileledger.async.RetrieveAccountsTask;
+import net.ktnx.mobileledger.model.LedgerAccount;
+import net.ktnx.mobileledger.ui.activity.MainActivity;
+import net.ktnx.mobileledger.utils.MLDB;
+
+import java.lang.ref.WeakReference;
+import java.util.Date;
+import java.util.List;
+
+import static net.ktnx.mobileledger.ui.activity.SettingsActivity.PREF_KEY_SHOW_ONLY_STARRED_ACCOUNTS;
+
+public class AccountSummaryFragment extends Fragment {
+
+ private static long account_list_last_updated;
+ private static boolean account_list_needs_update = true;
+ MenuItem mShowHiddenAccounts;
+ SharedPreferences.OnSharedPreferenceChangeListener sBindPreferenceSummaryToValueListener;
+ private AccountSummaryViewModel model;
+ private AccountSummaryAdapter modelAdapter;
+ private Menu optMenu;
+ private MainActivity mActivity;
+ private FloatingActionButton fab;
+ private SwipeRefreshLayout swiper;
+ @Override
+ public void onCreate(@Nullable Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setHasOptionsMenu(true);
+ }
+ public void onAttach(Context context) {
+ super.onAttach(context);
+ mActivity = (MainActivity) context;
+ }
+ @Override
+ public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
+ @Nullable Bundle savedInstanceState) {
+ return inflater.inflate(R.layout.account_summary_fragment, container, false);
+ }
+
+ @Override
+ public void onActivityCreated(@Nullable Bundle savedInstanceState) {
+ super.onActivityCreated(savedInstanceState);
+
+ model = ViewModelProviders.of(this).get(AccountSummaryViewModel.class);
+ List<LedgerAccount> accounts = model.getAccounts(this.getContext());
+ modelAdapter = new AccountSummaryAdapter(accounts);
+
+ RecyclerView root = mActivity.findViewById(R.id.account_root);
+ root.setAdapter(modelAdapter);
+
+ LinearLayoutManager llm = new LinearLayoutManager(mActivity);
+ llm.setOrientation(LinearLayoutManager.VERTICAL);
+ root.setLayoutManager(llm);
+
+ fab = mActivity.findViewById(R.id.btn_add_transaction);
+
+ root.addOnItemTouchListener(new RecyclerItemListener(mActivity, root,
+ new RecyclerItemListener.RecyclerTouchListener() {
+ @Override
+ public void onClickItem(View v, int position) {
+ Log.d("list", String.format("item %d clicked", position));
+ if (modelAdapter.isSelectionActive()) {
+ modelAdapter.selectItem(position);
+ }
+ }
+
+ @Override
+ public void onLongClickItem(View v, int position) {
+ Log.d("list", String.format("item %d long-clicked", position));
+ modelAdapter.startSelection();
+ if (optMenu != null) {
+ optMenu.findItem(R.id.menu_acc_summary_cancel_selection)
+ .setVisible(true);
+ optMenu.findItem(R.id.menu_acc_summary_confirm_selection)
+ .setVisible(true);
+ optMenu.findItem(R.id.menu_acc_summary_only_starred).setVisible(false);
+ }
+ {
+ if (fab != null) fab.hide();
+ }
+ }
+ }));
+
+ root.addOnScrollListener(new RecyclerView.OnScrollListener() {
+ @Override
+ public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
+ if (fab != null) {
+ if (dy < 0) fab.show();
+ if (dy > 0) fab.hide();
+ }
+ }
+ });
+ swiper = mActivity.findViewById(R.id.account_swiper);
+ swiper.setColorSchemeResources(R.color.colorPrimary, R.color.colorAccent);
+ swiper.setOnRefreshListener(() -> {
+ Log.d("ui", "refreshing accounts via swipe");
+ update_accounts(true);
+ });
+ prepare_db();
+// update_account_table();
+ update_accounts(false);
+
+ }
+ private void prepare_db() {
+ account_list_last_updated = MLDB.get_option_value(mActivity, "last_refresh", (long) 0);
+ }
+
+ private void update_accounts(boolean force) {
+ long now = new Date().getTime();
+ if ((now > (account_list_last_updated + (24 * 3600 * 1000))) || force) {
+ Log.d("db",
+ "accounts last updated at " + account_list_last_updated + " and now is " + now +
+ ". re-fetching");
+ update_accounts();
+ }
+ }
+
+ private void update_accounts() {
+ RetrieveAccountsTask task = new RetrieveAccountsTask(new WeakReference<>(this));
+
+ task.setPref(PreferenceManager.getDefaultSharedPreferences(mActivity));
+ task.execute();
+
+ }
+ public void onAccountRefreshDone(int error) {
+ swiper.setRefreshing(false);
+ if (error != 0) {
+ String err_text = getResources().getString(error);
+ Log.d("visual", String.format("showing snackbar: %s", err_text));
+ Snackbar.make(swiper, err_text, Snackbar.LENGTH_LONG).show();
+ }
+ else {
+ MLDB.set_option_value(mActivity, "last_refresh", new Date().getTime());
+ update_account_table();
+ }
+ }
+ private void update_account_table() {
+ if (this.getContext() == null) return;
+
+ model.reloadAccounts(this.getContext());
+ modelAdapter.notifyDataSetChanged();
+ }
+ public void onRefreshAccountSummaryClicked(MenuItem mi) {
+ update_accounts(true);
+ }
+
+ public void onShowOnlyStarredClicked(MenuItem mi) {
+ SharedPreferences pref = PreferenceManager.getDefaultSharedPreferences(mActivity);
+ boolean flag = pref.getBoolean(PREF_KEY_SHOW_ONLY_STARRED_ACCOUNTS, false);
+
+ SharedPreferences.Editor editor = pref.edit();
+ editor.putBoolean(PREF_KEY_SHOW_ONLY_STARRED_ACCOUNTS, !flag);
+ Log.d("pref", "Setting show only starred accounts pref to " + (flag ? "false" : "true"));
+ editor.apply();
+
+ update_account_table();
+ }
+
+ void stopSelection() {
+ modelAdapter.stopSelection();
+ if (optMenu != null) {
+ optMenu.findItem(R.id.menu_acc_summary_cancel_selection).setVisible(false);
+ optMenu.findItem(R.id.menu_acc_summary_confirm_selection).setVisible(false);
+ optMenu.findItem(R.id.menu_acc_summary_only_starred).setVisible(true);
+ }
+ {
+ if (fab != null) fab.show();
+ }
+ }
+ public void onCancelAccSelection(MenuItem item) {
+ stopSelection();
+ }
+ public void onConfirmAccSelection(MenuItem item) {
+ model.commitSelections(mActivity);
+ stopSelection();
+ }
+ @Override
+ public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
+ // Inflate the menu; this adds items to the action bar if it is present.
+ inflater.inflate(R.menu.account_summary, menu);
+ optMenu = menu;
+
+ mShowHiddenAccounts = menu.findItem(R.id.menu_acc_summary_only_starred);
+ if (mShowHiddenAccounts == null) throw new AssertionError();
+
+ sBindPreferenceSummaryToValueListener = (preference, value) -> mShowHiddenAccounts
+ .setChecked(preference.getBoolean(PREF_KEY_SHOW_ONLY_STARRED_ACCOUNTS, false));
+ SharedPreferences pref = PreferenceManager.getDefaultSharedPreferences(mActivity);
+ pref.registerOnSharedPreferenceChangeListener(sBindPreferenceSummaryToValueListener);
+
+ mShowHiddenAccounts.setChecked(pref.getBoolean(PREF_KEY_SHOW_ONLY_STARRED_ACCOUNTS, false));
+
+ Log.d("menu", "MainActivity: onCreateOptionsMenu called");
+ }
+}
--- /dev/null
+/*
+ * Copyright © 2018 Damyan Ivanov.
+ * This file is part of Mobile-Ledger.
+ * Mobile-Ledger is free software: you can distribute it and/or modify it
+ * under the term of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your opinion), any later version.
+ *
+ * Mobile-Ledger is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License terms for details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Mobile-Ledger. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package net.ktnx.mobileledger.ui.account_summary;
+
+import android.app.Application;
+import android.arch.lifecycle.AndroidViewModel;
+import android.content.Context;
+import android.content.res.Resources;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+import android.graphics.Typeface;
+import android.os.Build;
+import android.preference.PreferenceManager;
+import android.support.annotation.NonNull;
+import android.support.v7.widget.RecyclerView;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.CheckBox;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+import net.ktnx.mobileledger.R;
+import net.ktnx.mobileledger.model.LedgerAccount;
+import net.ktnx.mobileledger.utils.MLDB;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import static net.ktnx.mobileledger.ui.activity.SettingsActivity.PREF_KEY_SHOW_ONLY_STARRED_ACCOUNTS;
+
+class AccountSummaryViewModel extends AndroidViewModel {
+ private List<LedgerAccount> accounts;
+
+ public AccountSummaryViewModel(@NonNull Application application) {
+ super(application);
+ }
+
+ List<LedgerAccount> getAccounts(Context context) {
+ if (accounts == null) {
+ accounts = new ArrayList<>();
+ reloadAccounts(context);
+ }
+
+ return accounts;
+ }
+
+ void reloadAccounts(Context context) {
+ accounts.clear();
+ boolean showingOnlyStarred =
+ PreferenceManager.getDefaultSharedPreferences(getApplication())
+ .getBoolean(PREF_KEY_SHOW_ONLY_STARRED_ACCOUNTS, false);
+ String sql = "SELECT name, hidden FROM accounts";
+ if (showingOnlyStarred) sql += " WHERE hidden = 0";
+ sql += " ORDER BY name";
+
+ try (SQLiteDatabase db = MLDB.getReadableDatabase(context)) {
+ try (Cursor cursor = db
+ .rawQuery(sql,null))
+ {
+ while (cursor.moveToNext()) {
+ LedgerAccount acc = new LedgerAccount(cursor.getString(0));
+ acc.setHidden(cursor.getInt(1) == 1);
+ try (Cursor c2 = db.rawQuery(
+ "SELECT value, currency FROM account_values " + "WHERE account = ?",
+ new String[]{acc.getName()}))
+ {
+ while (c2.moveToNext()) {
+ acc.addAmount(c2.getFloat(0), c2.getString(1));
+ }
+ }
+ accounts.add(acc);
+ }
+ }
+ }
+ }
+ void commitSelections(Context context) {
+ try(SQLiteDatabase db = MLDB.getWritableDatabase(context)) {
+ db.beginTransaction();
+ try {
+ for (LedgerAccount acc : accounts) {
+ Log.d("db", String.format("Setting %s to %s", acc.getName(),
+ acc.isHidden() ? "hidden" : "starred"));
+ db.execSQL("UPDATE accounts SET hidden=? WHERE name=?",
+ new Object[]{acc.isHiddenToBe() ? 1 : 0, acc.getName()});
+ }
+ db.setTransactionSuccessful();
+ for (LedgerAccount acc : accounts ) { acc.setHidden(acc.isHiddenToBe()); }
+ }
+ finally { db.endTransaction(); }
+ }
+ }
+}
+
+class AccountSummaryAdapter extends RecyclerView.Adapter<AccountSummaryAdapter
+.LedgerRowHolder> {
+ private List<LedgerAccount> accounts;
+ private boolean selectionActive;
+
+ AccountSummaryAdapter(List<LedgerAccount> accounts) {
+ this.accounts = accounts;
+ this.selectionActive = false;
+ }
+
+ public void onBindViewHolder(@NonNull LedgerRowHolder holder, int position) {
+ LedgerAccount acc = accounts.get(position);
+ Context ctx = holder.row.getContext();
+ Resources rm = ctx.getResources();
+
+ holder.tvAccountName.setText(acc.getShortName());
+ holder.tvAccountName.setPadding(
+ acc.getLevel() * rm.getDimensionPixelSize(R.dimen.activity_horizontal_margin) / 2,
+ 0, 0,
+ 0);
+ holder.tvAccountAmounts.setText(acc.getAmountsString());
+
+ if (acc.isHidden()) {
+ holder.tvAccountName.setTypeface(null, Typeface.ITALIC);
+ holder.tvAccountAmounts.setTypeface(null, Typeface.ITALIC);
+ }
+ else {
+ holder.tvAccountName.setTypeface(null, Typeface.NORMAL);
+ holder.tvAccountAmounts.setTypeface(null, Typeface.NORMAL);
+ }
+
+ if (position % 2 == 0) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) holder.row
+ .setBackgroundColor(rm.getColor(R.color.table_row_even_bg, ctx.getTheme()));
+ else holder.row.setBackgroundColor(rm.getColor(R.color.table_row_even_bg));
+ }
+ else {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) holder.row
+ .setBackgroundColor(rm.getColor(R.color.drawer_background, ctx.getTheme()));
+ else holder.row.setBackgroundColor(rm.getColor(R.color.drawer_background));
+ }
+
+ holder.selectionCb.setVisibility( selectionActive ? View.VISIBLE : View.GONE);
+ holder.selectionCb.setChecked(!acc.isHiddenToBe());
+
+ holder.row.setTag(R.id.POS, position);
+ }
+
+ @NonNull
+ @Override
+ public LedgerRowHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
+ View row = LayoutInflater.from(parent.getContext())
+ .inflate(R.layout.account_summary_row, parent, false);
+ return new LedgerRowHolder(row);
+ }
+
+ @Override
+ public int getItemCount() {
+ return accounts.size();
+ }
+ public void startSelection() {
+ for( LedgerAccount acc : accounts ) acc.setHiddenToBe(acc.isHidden());
+ this.selectionActive = true;
+ notifyDataSetChanged();
+ }
+
+ public void stopSelection() {
+ this.selectionActive = false;
+ notifyDataSetChanged();
+ }
+
+ public boolean isSelectionActive() {
+ return selectionActive;
+ }
+
+ public void selectItem(int position) {
+ LedgerAccount acc = accounts.get(position);
+ acc.toggleHiddenToBe();
+ toggleChildrenOf(acc, acc.isHiddenToBe());
+ notifyDataSetChanged();
+ }
+ void toggleChildrenOf(LedgerAccount parent, boolean hiddenToBe) {
+ for (LedgerAccount acc : accounts) {
+ String acc_parent = acc.getParentName();
+ if ((acc_parent != null) && acc.getParentName().equals(parent.getName())) {
+ acc.setHiddenToBe(hiddenToBe);
+ toggleChildrenOf(acc, hiddenToBe);
+ }
+ }
+ }
+ class LedgerRowHolder extends RecyclerView.ViewHolder {
+ CheckBox selectionCb;
+ TextView tvAccountName, tvAccountAmounts;
+ LinearLayout row;
+ public LedgerRowHolder(@NonNull View itemView) {
+ super(itemView);
+ this.row = (LinearLayout) itemView;
+ this.tvAccountName = itemView.findViewById(R.id.account_row_acc_name);
+ this.tvAccountAmounts = itemView.findViewById(R.id.account_row_acc_amounts);
+ this.selectionCb = itemView.findViewById(R.id.account_row_check);
+ }
+ }
+}
\ No newline at end of file
--- /dev/null
+/*
+ * Copyright © 2018 Damyan Ivanov.
+ * This file is part of Mobile-Ledger.
+ * Mobile-Ledger is free software: you can distribute it and/or modify it
+ * under the term of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your opinion), any later version.
+ *
+ * Mobile-Ledger is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License terms for details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Mobile-Ledger. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package net.ktnx.mobileledger.ui.activity;
+
+import android.content.res.Configuration;
+import android.os.Bundle;
+import android.preference.PreferenceActivity;
+import android.support.annotation.LayoutRes;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.support.v7.app.ActionBar;
+import android.support.v7.app.AppCompatDelegate;
+import android.support.v7.widget.Toolbar;
+import android.view.MenuInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+/**
+ * A {@link android.preference.PreferenceActivity} which implements and proxies the necessary calls
+ * to be used with AppCompat.
+ */
+public abstract class AppCompatPreferenceActivity extends PreferenceActivity {
+
+ private AppCompatDelegate mDelegate;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ getDelegate().installViewFactory();
+ getDelegate().onCreate(savedInstanceState);
+ super.onCreate(savedInstanceState);
+ }
+
+ @Override
+ protected void onPostCreate(Bundle savedInstanceState) {
+ super.onPostCreate(savedInstanceState);
+ getDelegate().onPostCreate(savedInstanceState);
+ }
+
+ public ActionBar getSupportActionBar() {
+ return getDelegate().getSupportActionBar();
+ }
+
+ public void setSupportActionBar(@Nullable Toolbar toolbar) {
+ getDelegate().setSupportActionBar(toolbar);
+ }
+
+ @NonNull
+ @Override
+ public MenuInflater getMenuInflater() {
+ return getDelegate().getMenuInflater();
+ }
+
+ @Override
+ public void setContentView(@LayoutRes int layoutResID) {
+ getDelegate().setContentView(layoutResID);
+ }
+
+ @Override
+ public void setContentView(View view) {
+ getDelegate().setContentView(view);
+ }
+
+ @Override
+ public void setContentView(View view, ViewGroup.LayoutParams params) {
+ getDelegate().setContentView(view, params);
+ }
+
+ @Override
+ public void addContentView(View view, ViewGroup.LayoutParams params) {
+ getDelegate().addContentView(view, params);
+ }
+
+ @Override
+ protected void onPostResume() {
+ super.onPostResume();
+ getDelegate().onPostResume();
+ }
+
+ @Override
+ protected void onTitleChanged(CharSequence title, int color) {
+ super.onTitleChanged(title, color);
+ getDelegate().setTitle(title);
+ }
+
+ @Override
+ public void onConfigurationChanged(Configuration newConfig) {
+ super.onConfigurationChanged(newConfig);
+ getDelegate().onConfigurationChanged(newConfig);
+ }
+
+ @Override
+ protected void onStop() {
+ super.onStop();
+ getDelegate().onStop();
+ }
+
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+ getDelegate().onDestroy();
+ }
+
+ public void invalidateOptionsMenu() {
+ getDelegate().invalidateOptionsMenu();
+ }
+
+ private AppCompatDelegate getDelegate() {
+ if (mDelegate == null) {
+ mDelegate = AppCompatDelegate.create(this, null);
+ }
+ return mDelegate;
+ }
+}
--- /dev/null
+/*
+ * Copyright © 2018 Damyan Ivanov.
+ * This file is part of Mobile-Ledger.
+ * Mobile-Ledger is free software: you can distribute it and/or modify it
+ * under the term of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your opinion), any later version.
+ *
+ * Mobile-Ledger is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License terms for details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Mobile-Ledger. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package net.ktnx.mobileledger.ui.activity;
+
+import android.content.Intent;
+import android.content.pm.PackageInfo;
+import android.os.Build;
+import android.os.Bundle;
+import android.support.v4.app.Fragment;
+import android.support.v4.app.FragmentManager;
+import android.support.v4.app.FragmentTransaction;
+import android.support.v4.view.GravityCompat;
+import android.support.v4.widget.DrawerLayout;
+import android.support.v7.app.ActionBarDrawerToggle;
+import android.support.v7.app.AppCompatActivity;
+import android.support.v7.widget.Toolbar;
+import android.util.Log;
+import android.view.ContextMenu;
+import android.view.MenuItem;
+import android.view.View;
+import android.widget.LinearLayout;
+
+import net.ktnx.mobileledger.ui.account_summary.AccountSummaryFragment;
+import net.ktnx.mobileledger.R;
+import net.ktnx.mobileledger.ui.transaction_list.TransactionListFragment;
+
+public class MainActivity extends AppCompatActivity {
+ DrawerLayout drawer;
+ private AccountSummaryFragment accountSummaryFragment;
+ private TransactionListFragment transactionListFragment;
+ private Fragment currentFragment = null;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_account_summary);
+ Toolbar toolbar = findViewById(R.id.toolbar);
+ setSupportActionBar(toolbar);
+
+ drawer = findViewById(R.id.drawer_layout);
+ ActionBarDrawerToggle toggle =
+ new ActionBarDrawerToggle(this, drawer, toolbar, R.string.navigation_drawer_open,
+ R.string.navigation_drawer_close);
+ drawer.addDrawerListener(toggle);
+ toggle.syncState();
+
+ android.widget.TextView ver = drawer.findViewById(R.id.drawer_version_text);
+
+ try {
+ PackageInfo pi =
+ getApplicationContext().getPackageManager().getPackageInfo(getPackageName(), 0);
+ ver.setText(pi.versionName);
+ }
+ catch (Exception e) {
+ e.printStackTrace();
+ }
+
+ onAccountSummaryClicked(null);
+ }
+
+ @Override
+ protected void onStart() {
+ super.onStart();
+ LinearLayout grp = drawer.findViewById(R.id.nav_actions);
+ for (int i = 0; i < grp.getChildCount(); i++) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+ grp.getChildAt(i).setBackgroundColor(
+ getResources().getColor(R.color.drawer_background, getTheme()));
+ }
+ else {
+ grp.getChildAt(i)
+ .setBackgroundColor(getResources().getColor(R.color.drawer_background));
+ }
+ }
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+ drawer.findViewById(R.id.nav_account_summary).setBackgroundColor(
+ getResources().getColor(R.color.table_row_even_bg, getTheme()));
+ }
+ else {
+ drawer.findViewById(R.id.nav_account_summary)
+ .setBackgroundColor(getResources().getColor(R.color.table_row_even_bg));
+ }
+ }
+
+ public void fab_new_transaction_clicked(View view) {
+ Intent intent = new Intent(this, NewTransactionActivity.class);
+ startActivity(intent);
+ overridePendingTransition(R.anim.slide_in_right, R.anim.dummy);
+ }
+
+ public void nav_exit_clicked(View view) {
+ Log.w("app", "exiting");
+ finish();
+ }
+
+ public void nav_settings_clicked(View view) {
+ Intent intent = new Intent(this, SettingsActivity.class);
+ startActivity(intent);
+ }
+ private void markDrawerItemCurrent(int id) {
+ View item = drawer.findViewById(id);
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+ item.setBackgroundColor(getResources().getColor(R.color.table_row_even_bg, getTheme()));
+ }
+ else {
+ item.setBackgroundColor(getResources().getColor(R.color.table_row_even_bg));
+ }
+
+ LinearLayout actions = drawer.findViewById(R.id.nav_actions);
+ for (int i = 0; i < actions.getChildCount(); i++) {
+ View view = actions.getChildAt(i);
+ if (view.getId() != id) {
+ view.setBackgroundColor(getResources().getColor(android.R.color.transparent));
+ }
+ }
+ }
+ public void onOptionsMenuClicked(MenuItem menuItem) {
+ ContextMenu.ContextMenuInfo info = menuItem.getMenuInfo();
+ switch (menuItem.getItemId()) {
+ case R.id.menu_acc_summary_cancel_selection:
+ if (accountSummaryFragment != null)
+ accountSummaryFragment.onCancelAccSelection(menuItem);
+ break;
+ case R.id.menu_acc_summary_confirm_selection:
+ if (accountSummaryFragment != null)
+ accountSummaryFragment.onConfirmAccSelection(menuItem);
+ break;
+ case R.id.menu_acc_summary_only_starred:
+ if (accountSummaryFragment != null)
+ accountSummaryFragment.onShowOnlyStarredClicked(menuItem);
+ break;
+ case R.id.menu_transaction_list_filter:
+ if (transactionListFragment != null)
+ transactionListFragment.onShowFilterClick(menuItem);
+ break;
+ default:
+ Log.e("menu", String.format("Menu item %d not handled", menuItem.getItemId()));
+ }
+ }
+ public void onViewClicked(View view) {
+ switch (view.getId()) {
+ case R.id.clearAccountNameFilter:
+ if (transactionListFragment != null)
+ transactionListFragment.onClearAccountNameClick(view);
+ break;
+ default:
+ Log.e("click", String.format("View %d click not handled", view.getId()));
+ }
+ }
+ public void onAccountSummaryClicked(View view) {
+ markDrawerItemCurrent(R.id.nav_account_summary);
+ drawer.closeDrawers();
+
+ FragmentManager fm = getSupportFragmentManager();
+ FragmentTransaction ft = fm.beginTransaction();
+ currentFragment = accountSummaryFragment = new AccountSummaryFragment();
+ ft.replace(R.id.root_frame, accountSummaryFragment);
+ ft.commit();
+ }
+ public void onLatestTransactionsClicked(View view) {
+ markDrawerItemCurrent(R.id.nav_latest_transactions);
+ drawer.closeDrawers();
+
+ FragmentManager fm = getSupportFragmentManager();
+ FragmentTransaction ft = fm.beginTransaction();
+ currentFragment = transactionListFragment = new TransactionListFragment();
+ ft.replace(R.id.root_frame, transactionListFragment);
+ ft.commit();
+ }
+ @Override
+ public void onBackPressed() {
+ DrawerLayout drawer = findViewById(R.id.drawer_layout);
+ if (drawer.isDrawerOpen(GravityCompat.START)) {
+ drawer.closeDrawer(GravityCompat.START);
+ }
+ else {
+ super.onBackPressed();
+ }
+ }
+
+}
--- /dev/null
+/*
+ * Copyright © 2018 Damyan Ivanov.
+ * This file is part of Mobile-Ledger.
+ * Mobile-Ledger is free software: you can distribute it and/or modify it
+ * under the term of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your opinion), any later version.
+ *
+ * Mobile-Ledger is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License terms for details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Mobile-Ledger. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package net.ktnx.mobileledger.ui.activity;
+
+import android.annotation.SuppressLint;
+import android.os.Bundle;
+import android.preference.PreferenceManager;
+import android.support.design.widget.BaseTransientBottomBar;
+import android.support.design.widget.Snackbar;
+import android.support.v4.app.DialogFragment;
+import android.support.v7.app.AppCompatActivity;
+import android.support.v7.widget.Toolbar;
+import android.text.Editable;
+import android.text.InputType;
+import android.text.TextWatcher;
+import android.util.Log;
+import android.util.TypedValue;
+import android.view.Gravity;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.inputmethod.EditorInfo;
+import android.widget.AutoCompleteTextView;
+import android.widget.EditText;
+import android.widget.ProgressBar;
+import android.widget.TableLayout;
+import android.widget.TableRow;
+import android.widget.TextView;
+
+import net.ktnx.mobileledger.ui.OnSwipeTouchListener;
+import net.ktnx.mobileledger.R;
+import net.ktnx.mobileledger.async.SaveTransactionTask;
+import net.ktnx.mobileledger.async.TaskCallback;
+import net.ktnx.mobileledger.model.LedgerTransaction;
+import net.ktnx.mobileledger.model.LedgerTransactionAccount;
+import net.ktnx.mobileledger.ui.DatePickerFragment;
+import net.ktnx.mobileledger.utils.MLDB;
+
+import java.util.Date;
+import java.util.Objects;
+
+/*
+ * TODO: nicer progress while transaction is submitted
+ * TODO: latest transactions, maybe with browsing further in the past?
+ * TODO: reports
+ * TODO: get rid of the custom session/cookie and auth code?
+ * (the last problem with the POST was the missing content-length header)
+ * TODO: app icon
+ * TODO: nicer swiping removal with visual feedback
+ * TODO: setup wizard
+ * TODO: update accounts/check settings upon change of backend settings
+ * */
+
+public class NewTransactionActivity extends AppCompatActivity implements TaskCallback {
+ private static SaveTransactionTask saver;
+ private TableLayout table;
+ private ProgressBar progress;
+ private TextView text_date;
+ private AutoCompleteTextView text_descr;
+ private MenuItem mSave;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_new_transaction);
+ Toolbar toolbar = findViewById(R.id.toolbar);
+ setSupportActionBar(toolbar);
+
+ text_date = findViewById(R.id.new_transaction_date);
+ text_date.setOnFocusChangeListener(new View.OnFocusChangeListener() {
+ @Override
+ public void onFocusChange(View v, boolean hasFocus) {
+ if (hasFocus) pickTransactionDate(v);
+ }
+ });
+ text_descr = findViewById(R.id.new_transaction_description);
+ MLDB.hook_autocompletion_adapter(this, text_descr, MLDB.DESCRIPTION_HISTORY_TABLE,
+ "description");
+ hook_text_change_listener(text_descr);
+
+ progress = findViewById(R.id.save_transaction_progress);
+
+ Objects.requireNonNull(getSupportActionBar()).setDisplayHomeAsUpEnabled(true);
+ table = findViewById(R.id.new_transaction_accounts_table);
+ for (int i = 0; i < table.getChildCount(); i++) {
+ TableRow row = (TableRow) table.getChildAt(i);
+ AutoCompleteTextView acc_name_view = (AutoCompleteTextView) row.getChildAt(0);
+ TextView amount_view = (TextView) row.getChildAt(1);
+ hook_swipe_listener(row);
+ MLDB.hook_autocompletion_adapter(this, acc_name_view, MLDB.ACCOUNTS_TABLE, "name");
+ hook_text_change_listener(acc_name_view);
+ hook_text_change_listener(amount_view);
+// Log.d("swipe", "hooked to row "+i);
+ }
+ }
+
+ @Override
+ protected void onStart() {
+ super.onStart();
+ if (text_descr.getText().toString().isEmpty()) text_descr.requestFocus();
+ }
+
+ @Override
+ public void finish() {
+ super.finish();
+ overridePendingTransition(R.anim.dummy, R.anim.slide_out_right);
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ switch (item.getItemId()) {
+ case android.R.id.home:
+ finish();
+ return true;
+ }
+ return super.onOptionsItemSelected(item);
+ }
+
+ public void save_transaction() {
+ if (mSave != null) mSave.setVisible(false);
+ toggle_all_editing(false);
+ progress.setVisibility(View.VISIBLE);
+
+ saver = new SaveTransactionTask(this);
+
+ saver.setPref(PreferenceManager.getDefaultSharedPreferences(this));
+ String date = text_date.getText().toString();
+ if (date.isEmpty()) date = String.valueOf(new Date().getDate());
+ LedgerTransaction tr = new LedgerTransaction(date, text_descr.getText().toString());
+
+ TableLayout table = findViewById(R.id.new_transaction_accounts_table);
+ for (int i = 0; i < table.getChildCount(); i++) {
+ TableRow row = (TableRow) table.getChildAt(i);
+ String acc = ((TextView) row.getChildAt(0)).getText().toString();
+ String amt = ((TextView) row.getChildAt(1)).getText().toString();
+ LedgerTransactionAccount item =
+ amt.length() > 0 ? new LedgerTransactionAccount(acc, Float.parseFloat(amt))
+ : new LedgerTransactionAccount(acc);
+
+ tr.addAccount(item);
+ }
+ saver.execute(tr);
+ }
+
+ private void toggle_all_editing(boolean enabled) {
+ text_date.setEnabled(enabled);
+ text_descr.setEnabled(enabled);
+ TableLayout table = findViewById(R.id.new_transaction_accounts_table);
+ for (int i = 0; i < table.getChildCount(); i++) {
+ TableRow row = (TableRow) table.getChildAt(i);
+ for (int j = 0; j < row.getChildCount(); j++) {
+ row.getChildAt(j).setEnabled(enabled);
+ }
+ }
+ }
+
+ private void hook_swipe_listener(final TableRow row) {
+ row.getChildAt(0).setOnTouchListener(new OnSwipeTouchListener(this) {
+ public void onSwipeLeft() {
+// Log.d("swipe", "LEFT" + row.getId());
+ if (table.getChildCount() > 2) {
+ TableRow prev_row = (TableRow) table.getChildAt(table.indexOfChild(row) - 1);
+ TableRow next_row = (TableRow) table.getChildAt(table.indexOfChild(row) + 1);
+ TextView prev_amt =
+ (prev_row != null) ? (TextView) prev_row.getChildAt(1) : text_descr;
+ TextView next_acc =
+ (next_row != null) ? (TextView) next_row.getChildAt(0) : null;
+
+ if (next_acc == null) {
+ prev_amt.setNextFocusRightId(R.id.none);
+ prev_amt.setNextFocusForwardId(R.id.none);
+ prev_amt.setImeOptions(EditorInfo.IME_ACTION_DONE);
+ }
+ else {
+ prev_amt.setNextFocusRightId(next_acc.getId());
+ prev_amt.setNextFocusForwardId(next_acc.getId());
+ prev_amt.setImeOptions(EditorInfo.IME_ACTION_NEXT);
+ }
+
+ if (row.hasFocus()) {
+ if (next_acc != null) next_acc.requestFocus();
+ else prev_amt.requestFocus();
+ }
+
+ table.removeView(row);
+ check_transaction_submittable();
+// Toast.makeText(NewTransactionActivity.this, "LEFT", Toast.LENGTH_LONG).show();
+ }
+ else {
+ Snackbar.make(table, R.string.msg_at_least_two_accounts_are_required,
+ Snackbar.LENGTH_LONG).setAction("Action", null).show();
+ }
+ }
+ // @Override
+// public boolean performClick(View view, MotionEvent m) {
+// return true;
+// }
+ public boolean onTouch(View view, MotionEvent m) {
+ return gestureDetector.onTouchEvent(m);
+ }
+ });
+ }
+
+ private void hook_text_change_listener(final TextView view) {
+ view.addTextChangedListener(new TextWatcher() {
+ @Override
+ public void beforeTextChanged(CharSequence s, int start, int count, int after) {
+
+ }
+
+ @Override
+ public void onTextChanged(CharSequence s, int start, int before, int count) {
+
+ }
+
+ @Override
+ public void afterTextChanged(Editable s) {
+// Log.d("input", "text changed");
+ check_transaction_submittable();
+ }
+ });
+
+ }
+
+ public boolean onCreateOptionsMenu(Menu menu) {
+ // Inflate the menu; this adds items to the action bar if it is present.
+ getMenuInflater().inflate(R.menu.new_transaction, menu);
+ mSave = menu.findItem(R.id.action_submit_transaction);
+ if (mSave == null) throw new AssertionError();
+
+ check_transaction_submittable();
+
+ return true;
+ }
+
+ public void pickTransactionDate(View view) {
+ DialogFragment picker = new DatePickerFragment();
+ picker.show(getSupportFragmentManager(), "datePicker");
+ }
+
+ public int dp2px(float dp) {
+ return Math.round(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp,
+ getResources().getDisplayMetrics()));
+ }
+
+ private void do_add_account_row(boolean focus) {
+ final AutoCompleteTextView acc = new AutoCompleteTextView(this);
+ acc.setLayoutParams(new TableRow.LayoutParams(TableRow.LayoutParams.MATCH_PARENT,
+ TableRow.LayoutParams.WRAP_CONTENT, 9f));
+ acc.setHint(R.string.new_transaction_account_hint);
+ acc.setWidth(0);
+ acc.setImeOptions(EditorInfo.IME_ACTION_NEXT | EditorInfo.IME_FLAG_NO_ENTER_ACTION |
+ EditorInfo.IME_FLAG_NAVIGATE_NEXT);
+ acc.setSingleLine(true);
+
+ final EditText amt = new EditText(this);
+ amt.setLayoutParams(new TableRow.LayoutParams(TableRow.LayoutParams.WRAP_CONTENT,
+ TableRow.LayoutParams.MATCH_PARENT, 1f));
+ amt.setHint(R.string.new_transaction_amount_hint);
+ amt.setWidth(0);
+ amt.setInputType(InputType.TYPE_CLASS_NUMBER | InputType.TYPE_NUMBER_FLAG_SIGNED |
+ InputType.TYPE_NUMBER_FLAG_DECIMAL);
+ amt.setMinWidth(dp2px(40));
+ amt.setTextAlignment(EditText.TEXT_ALIGNMENT_VIEW_END);
+ amt.setImeOptions(EditorInfo.IME_ACTION_DONE);
+
+ // forward navigation support
+ final TableRow last_row = (TableRow) table.getChildAt(table.getChildCount() - 1);
+ final TextView last_amt = (TextView) last_row.getChildAt(1);
+ last_amt.setNextFocusForwardId(acc.getId());
+ last_amt.setNextFocusRightId(acc.getId());
+ last_amt.setImeOptions(EditorInfo.IME_ACTION_NEXT);
+ acc.setNextFocusForwardId(amt.getId());
+ acc.setNextFocusRightId(amt.getId());
+
+ final TableRow row = new TableRow(this);
+ row.setLayoutParams(new TableRow.LayoutParams(TableRow.LayoutParams.MATCH_PARENT,
+ TableRow.LayoutParams.MATCH_PARENT));
+ row.setGravity(Gravity.BOTTOM);
+ row.addView(acc);
+ row.addView(amt);
+ table.addView(row);
+
+ if (focus) acc.requestFocus();
+
+ hook_swipe_listener(row);
+ MLDB.hook_autocompletion_adapter(this, acc, MLDB.ACCOUNTS_TABLE, "name");
+ hook_text_change_listener(acc);
+ hook_text_change_listener(amt);
+ }
+
+ public void addTransactionAccountFromMenu(MenuItem item) {
+ do_add_account_row(true);
+ }
+
+ public void resetTransactionFromMenu(MenuItem item) {
+ reset_form();
+ }
+
+ public void saveTransactionFromMenu(MenuItem item) {
+ save_transaction();
+ }
+
+ private boolean is_zero(float f) {
+ return (f < 0.005) && (f > -0.005);
+ }
+
+ // rules:
+ // 1) at least two account names
+ // 2) each amount must have account name
+ // 3) amounts must balance to 0, or
+ // 3a) there must be exactly one empty amount
+ // 4) empty accounts with empty amounts are ignored
+ // 5) a row with an empty account name or empty amount is guaranteed to exist
+ @SuppressLint("DefaultLocale")
+ private void check_transaction_submittable() {
+ TableLayout table = findViewById(R.id.new_transaction_accounts_table);
+ int accounts = 0;
+ int accounts_with_values = 0;
+ int amounts = 0;
+ int amounts_with_accounts = 0;
+ int empty_rows = 0;
+ TextView empty_amount = null;
+ boolean single_empty_amount = false;
+ boolean single_empty_amount_has_account = false;
+ float running_total = 0f;
+ boolean have_description =
+ !((TextView) findViewById(R.id.new_transaction_description)).getText().toString()
+ .isEmpty();
+
+ try {
+ for (int i = 0; i < table.getChildCount(); i++) {
+ TableRow row = (TableRow) table.getChildAt(i);
+
+ TextView acc_name_v = (TextView) row.getChildAt(0);
+ TextView amount_v = (TextView) row.getChildAt(1);
+ String amt = String.valueOf(amount_v.getText());
+ String acc_name = String.valueOf(acc_name_v.getText());
+ acc_name = acc_name.trim();
+
+ if (!acc_name.isEmpty()) {
+ accounts++;
+
+ if (!amt.isEmpty()) {
+ accounts_with_values++;
+ }
+ }
+ else empty_rows++;
+
+ if (amt.isEmpty()) {
+ amount_v.setHint(String.format("%1.2f", 0f));
+ if (empty_amount == null) {
+ empty_amount = amount_v;
+ single_empty_amount = true;
+ single_empty_amount_has_account = !acc_name.isEmpty();
+ }
+ else if (!acc_name.isEmpty()) single_empty_amount = false;
+ }
+ else {
+ amounts++;
+ if (!acc_name.isEmpty()) amounts_with_accounts++;
+ running_total += Float.valueOf(amt);
+ }
+ }
+
+ if ((empty_rows == 0) &&
+ ((table.getChildCount() == accounts) || (table.getChildCount() == amounts)))
+ {
+ do_add_account_row(false);
+ }
+
+ Log.d("submittable", String.format("accounts=%d, accounts_with_values=%s, " +
+ "amounts_with_accounts=%d, amounts=%d, running_total=%1.2f, " +
+ "single_empty_with_acc=%s", accounts,
+ accounts_with_values, amounts_with_accounts, amounts, running_total,
+ (single_empty_amount && single_empty_amount_has_account) ? "true" : "false"));
+
+ if (have_description && (accounts >= 2) && (accounts_with_values >= (accounts - 1)) &&
+ (amounts_with_accounts == amounts) &&
+ (single_empty_amount && single_empty_amount_has_account || is_zero(running_total)))
+ {
+ if (mSave != null) mSave.setVisible(true);
+ }
+ else if (mSave != null) mSave.setVisible(false);
+
+ if (single_empty_amount) {
+ empty_amount.setHint(String.format("%1.2f",
+ (Math.abs(running_total) > 0.005) ? -running_total : 0f));
+ }
+
+ }
+ catch (NumberFormatException e) {
+ if (mSave != null) mSave.setVisible(false);
+ }
+ catch (Exception e) {
+ e.printStackTrace();
+ if (mSave != null) mSave.setVisible(false);
+ }
+ }
+
+ @Override
+ public void done(String error) {
+ progress.setVisibility(View.INVISIBLE);
+ Log.d("visuals", "hiding progress");
+
+ if (error == null) reset_form();
+ else Snackbar.make(findViewById(R.id.new_transaction_accounts_table), error,
+ BaseTransientBottomBar.LENGTH_LONG).show();
+
+ toggle_all_editing(true);
+ check_transaction_submittable();
+ }
+
+ private void reset_form() {
+ text_date.setText("");
+ text_descr.setText("");
+
+ text_descr.requestFocus();
+
+ while (table.getChildCount() > 2) {
+ table.removeViewAt(2);
+ }
+ for (int i = 0; i < 2; i++) {
+ TableRow tr = (TableRow) table.getChildAt(i);
+ if (tr == null) break;
+
+ ((TextView) tr.getChildAt(0)).setText("");
+ ((TextView) tr.getChildAt(1)).setText("");
+ }
+ }
+}
--- /dev/null
+/*
+ * Copyright © 2018 Damyan Ivanov.
+ * This file is part of Mobile-Ledger.
+ * Mobile-Ledger is free software: you can distribute it and/or modify it
+ * under the term of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your opinion), any later version.
+ *
+ * Mobile-Ledger is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License terms for details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Mobile-Ledger. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package net.ktnx.mobileledger.ui.activity;
+
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.content.Intent;
+import android.content.res.Configuration;
+import android.media.Ringtone;
+import android.media.RingtoneManager;
+import android.net.Uri;
+import android.os.Build;
+import android.os.Bundle;
+import android.preference.ListPreference;
+import android.preference.Preference;
+import android.preference.PreferenceActivity;
+import android.preference.PreferenceFragment;
+import android.preference.PreferenceManager;
+import android.preference.RingtonePreference;
+import android.support.v4.app.NavUtils;
+import android.support.v7.app.ActionBar;
+import android.text.TextUtils;
+import android.view.MenuItem;
+
+import net.ktnx.mobileledger.R;
+
+import java.util.List;
+
+/**
+ * A {@link PreferenceActivity} that presents a set of application settings. On
+ * handset devices, settings are presented as a single list. On tablets,
+ * settings are split by category, with category headers shown to the left of
+ * the list of settings.
+ * <p>
+ * See <a href="http://developer.android.com/design/patterns/settings.html">
+ * Android Design: Settings</a> for design guidelines and the <a
+ * href="http://developer.android.com/guide/topics/ui/settings.html">Settings
+ * API Guide</a> for more information on developing a Settings UI.
+ */
+public class SettingsActivity extends AppCompatPreferenceActivity {
+ public static String PREF_KEY_SHOW_ONLY_STARRED_ACCOUNTS = "pref_show_only_starred_accounts";
+
+ /**
+ * A preference value change listener that updates the preference's summary
+ * to reflect its new value.
+ */
+ private static Preference.OnPreferenceChangeListener sBindPreferenceSummaryToValueListener = (preference, value) -> {
+ String stringValue = value.toString();
+
+ if (preference instanceof ListPreference) {
+ // For list preferences, look up the correct display value in
+ // the preference's 'entries' list.
+ ListPreference listPreference = (ListPreference) preference;
+ int index = listPreference.findIndexOfValue(stringValue);
+
+ // Set the summary to reflect the new value.
+ preference.setSummary(
+ index >= 0
+ ? listPreference.getEntries()[index]
+ : null);
+
+ } else if (preference instanceof RingtonePreference) {
+ // For ringtone preferences, look up the correct display value
+ // using RingtoneManager.
+ if (TextUtils.isEmpty(stringValue)) {
+ // Empty values correspond to 'silent' (no ringtone).
+ preference.setSummary(R.string.pref_ringtone_silent);
+
+ } else {
+ Ringtone ringtone = RingtoneManager.getRingtone(
+ preference.getContext(), Uri.parse(stringValue));
+
+ if (ringtone == null) {
+ // Clear the summary if there was a lookup error.
+ preference.setSummary(null);
+ } else {
+ // Set the summary to reflect the new ringtone display
+ // name.
+ String name = ringtone.getTitle(preference.getContext());
+ preference.setSummary(name);
+ }
+ }
+ } else {
+ // For all other preferences, set the summary to the value's
+ // simple string representation.
+ preference.setSummary(stringValue);
+ }
+
+ return true;
+ };
+
+ /**
+ * Helper method to determine if the device has an extra-large screen. For
+ * example, 10" tablets are extra-large.
+ */
+ private static boolean isXLargeTablet(Context context) {
+ return (context.getResources().getConfiguration().screenLayout
+ & Configuration.SCREENLAYOUT_SIZE_MASK) >= Configuration.SCREENLAYOUT_SIZE_XLARGE;
+ }
+
+ /**
+ * Binds a preference's summary to its value. More specifically, when the
+ * preference's value is changed, its summary (line of text below the
+ * preference title) is updated to reflect the value. The summary is also
+ * immediately updated upon calling this method. The exact display format is
+ * dependent on the type of preference.
+ *
+ * @see #sBindPreferenceSummaryToValueListener
+ */
+ private static void bindPreferenceSummaryToValue(Preference preference) {
+ // Set the listener to watch for value changes.
+ preference.setOnPreferenceChangeListener(sBindPreferenceSummaryToValueListener);
+
+ // Trigger the listener immediately with the preference's
+ // current value.
+ sBindPreferenceSummaryToValueListener.onPreferenceChange(preference,
+ PreferenceManager
+ .getDefaultSharedPreferences(preference.getContext())
+ .getString(preference.getKey(), ""));
+ }
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setupActionBar();
+ }
+
+ /**
+ * Set up the {@link android.app.ActionBar}, if the API is available.
+ */
+ private void setupActionBar() {
+ ActionBar actionBar = getSupportActionBar();
+ if (actionBar != null) {
+ // Show the Up button in the action bar.
+ actionBar.setDisplayHomeAsUpEnabled(true);
+ }
+ }
+
+ @Override
+ public boolean onMenuItemSelected(int featureId, MenuItem item) {
+ int id = item.getItemId();
+ if (id == android.R.id.home) {
+ if (!super.onMenuItemSelected(featureId, item)) {
+ NavUtils.navigateUpFromSameTask(this);
+ }
+ return true;
+ }
+ return super.onMenuItemSelected(featureId, item);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public boolean onIsMultiPane() {
+ return isXLargeTablet(this);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ @TargetApi(Build.VERSION_CODES.HONEYCOMB)
+ public void onBuildHeaders(List<Header> target) {
+ loadHeadersFromResource(R.xml.pref_headers, target);
+ }
+
+ /**
+ * This method stops fragment injection in malicious applications.
+ * Make sure to deny any unknown fragments here.
+ */
+ protected boolean isValidFragment(String fragmentName) {
+ return PreferenceFragment.class.getName().equals(fragmentName)
+ || BackendPreferenceFragment.class.getName().equals(fragmentName)
+ || DataSyncPreferenceFragment.class.getName().equals(fragmentName)
+ || NotificationPreferenceFragment.class.getName().equals(fragmentName)
+ || InterfacePreferenceFragment.class.getName().equals(fragmentName);
+ }
+
+ /**
+ * This fragment shows general preferences only. It is used when the
+ * activity is showing a two-pane settings UI.
+ */
+ @TargetApi(Build.VERSION_CODES.HONEYCOMB)
+ public static class BackendPreferenceFragment extends PreferenceFragment {
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ addPreferencesFromResource(R.xml.pref_backend);
+ setHasOptionsMenu(true);
+
+ // Bind the summaries of EditText/List/Dialog/Ringtone preferences
+ // to their values. When their values change, their summaries are
+ // updated to reflect the new value, per the Android Design
+ // guidelines.
+ bindPreferenceSummaryToValue(findPreference("backend_url"));
+ bindPreferenceSummaryToValue(findPreference("backend_auth_user"));
+
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ int id = item.getItemId();
+ if (id == android.R.id.home) {
+ startActivity(new Intent(getActivity(), SettingsActivity.class));
+ return true;
+ }
+ return super.onOptionsItemSelected(item);
+ }
+ }
+
+ /**
+ * This fragment shows general preferences only. It is used when the
+ * activity is showing a two-pane settings UI.
+ */
+ @TargetApi(Build.VERSION_CODES.HONEYCOMB)
+ public static
+ class InterfacePreferenceFragment extends PreferenceFragment {
+ @Override
+ public
+ void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ addPreferencesFromResource(R.xml.pref_interface);
+ setHasOptionsMenu(true);
+
+ // Bind the summaries of EditText/List/Dialog/Ringtone preferences
+ // to their values. When their values change, their summaries are
+ // updated to reflect the new value, per the Android Design
+ // guidelines.
+
+ }
+
+ @Override
+ public
+ boolean onOptionsItemSelected(MenuItem item) {
+ int id = item.getItemId();
+ if (id == android.R.id.home) {
+ startActivity(new Intent(getActivity(), SettingsActivity.class));
+ return true;
+ }
+ return super.onOptionsItemSelected(item);
+ }
+ }
+
+ /**
+ * This fragment shows notification preferences only. It is used when the
+ * activity is showing a two-pane settings UI.
+ */
+ @TargetApi(Build.VERSION_CODES.HONEYCOMB)
+ public static class NotificationPreferenceFragment extends PreferenceFragment {
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ addPreferencesFromResource(R.xml.pref_notification);
+ setHasOptionsMenu(true);
+
+ // Bind the summaries of EditText/List/Dialog/Ringtone preferences
+ // to their values. When their values change, their summaries are
+ // updated to reflect the new value, per the Android Design
+ // guidelines.
+ bindPreferenceSummaryToValue(findPreference("notifications_new_message_ringtone"));
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ int id = item.getItemId();
+ if (id == android.R.id.home) {
+ startActivity(new Intent(getActivity(), SettingsActivity.class));
+ return true;
+ }
+ return super.onOptionsItemSelected(item);
+ }
+ }
+
+ /**
+ * This fragment shows data and sync preferences only. It is used when the
+ * activity is showing a two-pane settings UI.
+ */
+ @TargetApi(Build.VERSION_CODES.HONEYCOMB)
+ public static class DataSyncPreferenceFragment extends PreferenceFragment {
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ addPreferencesFromResource(R.xml.pref_data_sync);
+ setHasOptionsMenu(true);
+
+ // Bind the summaries of EditText/List/Dialog/Ringtone preferences
+ // to their values. When their values change, their summaries are
+ // updated to reflect the new value, per the Android Design
+ // guidelines.
+ bindPreferenceSummaryToValue(findPreference("sync_frequency"));
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ int id = item.getItemId();
+ if (id == android.R.id.home) {
+ startActivity(new Intent(getActivity(), SettingsActivity.class));
+ return true;
+ }
+ return super.onOptionsItemSelected(item);
+ }
+ }
+}
--- /dev/null
+/*
+ * Copyright © 2018 Damyan Ivanov.
+ * This file is part of Mobile-Ledger.
+ * Mobile-Ledger is free software: you can distribute it and/or modify it
+ * under the term of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your opinion), any later version.
+ *
+ * Mobile-Ledger is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License terms for details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Mobile-Ledger. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package net.ktnx.mobileledger.ui.transaction_list;
+
+import android.content.Context;
+import android.database.sqlite.SQLiteDatabase;
+import android.graphics.Typeface;
+import android.support.annotation.NonNull;
+import android.support.constraint.ConstraintLayout;
+import android.support.v7.widget.AppCompatTextView;
+import android.support.v7.widget.RecyclerView;
+import android.util.Log;
+import android.view.Gravity;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+import net.ktnx.mobileledger.R;
+import net.ktnx.mobileledger.model.LedgerTransaction;
+import net.ktnx.mobileledger.model.LedgerTransactionAccount;
+import net.ktnx.mobileledger.utils.Globals;
+import net.ktnx.mobileledger.utils.MLDB;
+
+import static net.ktnx.mobileledger.utils.DimensionUtils.dp2px;
+
+public class TransactionListAdapter
+ extends RecyclerView.Adapter<TransactionListAdapter.TransactionRowHolder> {
+ TransactionListViewModel model;
+ private String boldAccountName;
+ public TransactionListAdapter(TransactionListViewModel model) {
+ this.model = model;
+ }
+ public void onBindViewHolder(@NonNull TransactionRowHolder holder, int position) {
+ LedgerTransaction tr = model.getTransaction(position);
+ // in a race when transaction list is reduced, but the model hasn't been notified yet
+ // the view will disappear when the notifications reaches the model, so by simply omitting
+ // the out-of-range get() call nothing bad happens - just a to-be-deleted view remains
+ // a bit longer
+ if (tr == null) return;
+
+ Context ctx = holder.row.getContext();
+
+ try (SQLiteDatabase db = MLDB.getReadableDatabase(ctx)) {
+ tr.loadData(db);
+ holder.tvDescription.setText(tr.getDescription());
+ holder.tvDate.setText(tr.getDate());
+
+ int rowIndex = 0;
+ for (LedgerTransactionAccount acc : tr.getAccounts()) {
+ LinearLayout row = (LinearLayout) holder.tableAccounts.getChildAt(rowIndex++);
+ TextView accName, accAmount;
+ if (row == null) {
+ row = new LinearLayout(ctx);
+ row.setLayoutParams(
+ new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT,
+ LinearLayout.LayoutParams.WRAP_CONTENT));
+ row.setGravity(Gravity.CENTER_VERTICAL);
+ row.setOrientation(LinearLayout.HORIZONTAL);
+ row.setPaddingRelative(dp2px(ctx, 8), 0, 0, 0);
+ accName = new AppCompatTextView(ctx);
+ accName.setLayoutParams(
+ new LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT,
+ 5f));
+ accName.setTextAlignment(View.TEXT_ALIGNMENT_VIEW_START);
+ row.addView(accName);
+ accAmount = new AppCompatTextView(ctx);
+ LinearLayout.LayoutParams llp =
+ new LinearLayout.LayoutParams(LinearLayout.LayoutParams.WRAP_CONTENT,
+ LinearLayout.LayoutParams.WRAP_CONTENT);
+ llp.setMarginEnd(0);
+ accAmount.setLayoutParams(llp);
+ accAmount.setTextAlignment(View.TEXT_ALIGNMENT_VIEW_END);
+ accAmount.setMinWidth(dp2px(ctx, 60));
+ row.addView(accAmount);
+ holder.tableAccounts.addView(row);
+ }
+ else {
+ accName = (TextView) row.getChildAt(0);
+ accAmount = (TextView) row.getChildAt(1);
+ }
+ accName.setText(acc.getAccountName());
+ accAmount.setText(acc.toString());
+
+ if ((boldAccountName != null) && boldAccountName.equals(acc.getAccountName())) {
+ accName.setTypeface(null, Typeface.BOLD);
+ accAmount.setTypeface(null, Typeface.BOLD);
+ accName.setTextColor(Globals.primaryDark);
+ accAmount.setTextColor(Globals.primaryDark);
+ }
+ else {
+ accName.setTypeface(null, Typeface.NORMAL);
+ accAmount.setTypeface(null, Typeface.NORMAL);
+ accName.setTextColor(Globals.defaultTextColor);
+ accAmount.setTextColor(Globals.defaultTextColor);
+ }
+
+ }
+ if (holder.tableAccounts.getChildCount() > rowIndex) {
+ holder.tableAccounts
+ .removeViews(rowIndex, holder.tableAccounts.getChildCount() - rowIndex);
+ }
+
+ if (position % 2 == 0) {
+ holder.row.setBackgroundColor(Globals.table_row_even_bg);
+ }
+ else {
+ holder.row.setBackgroundColor(Globals.table_row_odd_bg);
+ }
+
+ Log.d("transactions", String.format("Filled position %d", position));
+ }
+ }
+
+ @NonNull
+ @Override
+ public TransactionRowHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
+ Log.d("perf", "onCreateViewHolder called");
+ View row = LayoutInflater.from(parent.getContext())
+ .inflate(R.layout.transaction_list_row, parent, false);
+ return new TransactionRowHolder(row);
+ }
+
+ @Override
+ public int getItemCount() {
+ return model.getTransactionCount();
+ }
+ public void setBoldAccountName(String boldAccountName) {
+ this.boldAccountName = boldAccountName;
+ }
+ public void resetBoldAccountName() {
+ this.boldAccountName = null;
+ }
+ class TransactionRowHolder extends RecyclerView.ViewHolder {
+ TextView tvDescription, tvDate;
+ LinearLayout tableAccounts;
+ ConstraintLayout row;
+ public TransactionRowHolder(@NonNull View itemView) {
+ super(itemView);
+ this.row = itemView.findViewById(R.id.transaction_row);
+ this.tvDescription = itemView.findViewById(R.id.transaction_row_description);
+ this.tvDate = itemView.findViewById(R.id.transaction_row_date);
+ this.tableAccounts = itemView.findViewById(R.id.transaction_row_acc_amounts);
+ }
+ }
+}
\ No newline at end of file
package net.ktnx.mobileledger.ui.transaction_list;
import android.arch.lifecycle.ViewModelProviders;
+import android.content.Context;
+import android.database.MatrixCursor;
+import android.os.Build;
import android.os.Bundle;
+import android.preference.PreferenceManager;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.app.Fragment;
+import android.support.v4.widget.SwipeRefreshLayout;
+import android.support.v7.widget.LinearLayoutManager;
+import android.support.v7.widget.RecyclerView;
import android.util.Log;
import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
+import android.view.inputmethod.InputMethodManager;
+import android.widget.AdapterView;
+import android.widget.AutoCompleteTextView;
+import android.widget.LinearLayout;
+import android.widget.ProgressBar;
+import android.widget.TextView;
+import net.ktnx.mobileledger.ui.activity.MainActivity;
import net.ktnx.mobileledger.R;
+import net.ktnx.mobileledger.async.RetrieveTransactionsTask;
+import net.ktnx.mobileledger.utils.Globals;
+import net.ktnx.mobileledger.utils.MLDB;
-public class TransactionListFragment extends Fragment {
+import java.lang.ref.WeakReference;
+import java.time.ZoneId;
+import java.time.format.DateTimeFormatter;
+import java.util.Date;
+
+import static android.content.Context.INPUT_METHOD_SERVICE;
- private TransactionListViewModel mViewModel;
- public static TransactionListFragment newInstance() {
- return new TransactionListFragment();
+public class TransactionListFragment extends Fragment {
+ public TransactionListViewModel model;
+ private MainActivity mActivity;
+ private View bTransactionListCancelDownload;
+ private MenuItem menuTransactionListFilter;
+ private View vAccountFilter;
+ private SwipeRefreshLayout swiper;
+ private RecyclerView root;
+ private ProgressBar progressBar;
+ private LinearLayout progressLayout;
+ private TextView tvLastUpdate;
+ private TransactionListAdapter modelAdapter;
+ private RetrieveTransactionsTask retrieveTransactionsTask;
+ private AutoCompleteTextView accNameFilter;
+ @Override
+ public void onCreate(@Nullable Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setHasOptionsMenu(true);
+ }
+ @Override
+ public void onAttach(Context context) {
+ super.onAttach(context);
+ mActivity = (MainActivity) context;
}
@Nullable
@Override
public void onActivityCreated(@Nullable Bundle savedInstanceState) {
Log.d("flow", "TransactionListFragment.onActivityCreated called");
super.onActivityCreated(savedInstanceState);
- mViewModel = ViewModelProviders.of(this).get(TransactionListViewModel.class);
- // TODO: Use the ViewModel
+
+ swiper = mActivity.findViewById(R.id.transaction_swipe);
+ if (swiper == null) throw new RuntimeException("Can't get hold on the swipe layout");
+ root = mActivity.findViewById(R.id.transaction_root);
+ if (root == null) throw new RuntimeException("Can't get hold on the transaction list view");
+ progressBar = mActivity.findViewById(R.id.transaction_list_progress_bar);
+ if (progressBar == null)
+ throw new RuntimeException("Can't get hold on the transaction list progress bar");
+ progressLayout = mActivity.findViewById(R.id.transaction_progress_layout);
+ if (progressLayout == null) throw new RuntimeException(
+ "Can't get hold on the transaction list progress bar layout");
+ tvLastUpdate = mActivity.findViewById(R.id.transactions_last_update);
+ updateLastUpdateText();
+ model = ViewModelProviders.of(this).get(TransactionListViewModel.class);
+ modelAdapter = new TransactionListAdapter(model);
+
+ RecyclerView root = mActivity.findViewById(R.id.transaction_root);
+ root.setAdapter(modelAdapter);
+
+ LinearLayoutManager llm = new LinearLayoutManager(mActivity);
+
+ llm.setOrientation(LinearLayoutManager.VERTICAL);
+ root.setLayoutManager(llm);
+
+ swiper.setOnRefreshListener(() -> {
+ Log.d("ui", "refreshing transactions via swipe");
+ update_transactions();
+ });
+
+ swiper.setColorSchemeResources(R.color.colorPrimary, R.color.colorAccent);
+
+ vAccountFilter = mActivity.findViewById(R.id.transaction_list_account_name_filter);
+ accNameFilter = mActivity.findViewById(R.id.transaction_filter_account_name);
+ bTransactionListCancelDownload =
+ mActivity.findViewById(R.id.transaction_list_cancel_download);
+
+ TransactionListFragment me = this;
+ MLDB.hook_autocompletion_adapter(mActivity, accNameFilter, "accounts", "name");
+ accNameFilter.setOnItemClickListener(new AdapterView.OnItemClickListener() {
+ @Override
+ public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
+ Log.d("tmp", "direct onItemClick");
+ model.reloadTransactions(me);
+ MatrixCursor mc = (MatrixCursor) parent.getItemAtPosition(position);
+ modelAdapter.setBoldAccountName(mc.getString(1));
+ modelAdapter.notifyDataSetChanged();
+ Globals.hideSoftKeyboard(mActivity);
+ }
+ });
+
+ updateLastUpdateText();
+ long last_update = MLDB.get_option_value(mActivity, MLDB.OPT_TRANSACTION_LIST_STAMP, 0L);
+ Log.d("transactions", String.format("Last update = %d", last_update));
+ if (last_update == 0) {
+ update_transactions();
+ }
+ else {
+ model.reloadTransactions(this);
+ }
+ }
+ @Override
+ public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
+ inflater.inflate(R.menu.transaction_list, menu);
+
+ menuTransactionListFilter = menu.findItem(R.id.menu_transaction_list_filter);
+ if ((menuTransactionListFilter == null)) throw new AssertionError();
+
+ super.onCreateOptionsMenu(menu, inflater);
+ }
+ private void update_transactions() {
+ retrieveTransactionsTask = new RetrieveTransactionsTask(new WeakReference<>(this));
+
+ RetrieveTransactionsTask.Params params = new RetrieveTransactionsTask.Params(
+ PreferenceManager.getDefaultSharedPreferences(mActivity));
+
+ retrieveTransactionsTask.execute(params);
+ bTransactionListCancelDownload.setEnabled(true);
+ }
+ public void onRetrieveStart() {
+ progressBar.setIndeterminate(true);
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) progressBar.setProgress(0, false);
+ else progressBar.setProgress(0);
+ progressLayout.setVisibility(View.VISIBLE);
+ }
+ public void onRetrieveProgress(RetrieveTransactionsTask.Progress progress) {
+ if ((progress.getTotal() == RetrieveTransactionsTask.Progress.INDETERMINATE) ||
+ (progress.getTotal() == 0))
+ {
+ progressBar.setIndeterminate(true);
+ }
+ else {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ progressBar.setMin(0);
+ }
+ progressBar.setMax(progress.getTotal());
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
+ progressBar.setProgress(progress.getProgress(), true);
+ }
+ else progressBar.setProgress(progress.getProgress());
+ progressBar.setIndeterminate(false);
+ }
}
+ public void onRetrieveDone(boolean success) {
+ progressLayout.setVisibility(View.GONE);
+ swiper.setRefreshing(false);
+ updateLastUpdateText();
+ if (success) {
+ Log.d("transactions", "calling notifyDataSetChanged()");
+ modelAdapter.notifyDataSetChanged();
+ }
+ }
+ private void updateLastUpdateText() {
+ {
+ long last_update =
+ MLDB.get_option_value(mActivity, MLDB.OPT_TRANSACTION_LIST_STAMP, 0L);
+ Log.d("transactions", String.format("Last update = %d", last_update));
+ if (last_update == 0) {
+ tvLastUpdate.setText(getString(R.string.transaction_last_update_never));
+ }
+ else {
+ Date date = new Date(last_update);
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ tvLastUpdate.setText(date.toInstant().atZone(ZoneId.systemDefault())
+ .format(DateTimeFormatter.ISO_LOCAL_DATE_TIME));
+ }
+ else {
+ tvLastUpdate.setText(date.toLocaleString());
+ }
+ }
+ }
+ }
+ public void onClearAccountNameClick(View view) {
+ vAccountFilter.setVisibility(View.GONE);
+ menuTransactionListFilter.setVisible(true);
+ accNameFilter.setText(null);
+ model.reloadTransactions(this);
+ modelAdapter.resetBoldAccountName();
+ modelAdapter.notifyDataSetChanged();
+ Globals.hideSoftKeyboard(mActivity);
+ }
+ public void onShowFilterClick(MenuItem menuItem) {
+ vAccountFilter.setVisibility(View.VISIBLE);
+ menuTransactionListFilter.setVisible(false);
+ accNameFilter.requestFocus();
+ InputMethodManager imm =
+ (InputMethodManager) mActivity.getSystemService(INPUT_METHOD_SERVICE);
+ imm.showSoftInput(accNameFilter, 0);
+ }
+ public void onStopTransactionRefreshClick(View view) {
+ Log.d("interactive", "Cancelling transactions refresh");
+ if (retrieveTransactionsTask != null) retrieveTransactionsTask.cancel(false);
+ bTransactionListCancelDownload.setEnabled(false);
+ }
}
package net.ktnx.mobileledger.ui.transaction_list;
+import android.app.Activity;
import android.arch.lifecycle.ViewModel;
-import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.util.Log;
import android.widget.AutoCompleteTextView;
import net.ktnx.mobileledger.R;
-import net.ktnx.mobileledger.TransactionListActivity;
import net.ktnx.mobileledger.model.LedgerTransaction;
import net.ktnx.mobileledger.utils.MLDB;
private ArrayList<LedgerTransaction> transactions;
- public void reloadTransactions(Context context) {
+ public void reloadTransactions(TransactionListFragment context) {
ArrayList<LedgerTransaction> newList = new ArrayList<>();
- TransactionListActivity act = (TransactionListActivity) context;
+ Activity act = context.getActivity();
+
boolean hasFilter =
act.findViewById(R.id.transaction_list_account_name_filter).getVisibility() ==
View.VISIBLE;
}
Log.d("tmp", sql);
- try (SQLiteDatabase db = MLDB.getReadableDatabase(context)) {
+ try (SQLiteDatabase db = MLDB.getReadableDatabase(act)) {
try (Cursor cursor = db.rawQuery(sql, params)) {
while (cursor.moveToNext()) {
newList.add(new LedgerTransaction(cursor.getInt(0)));
package net.ktnx.mobileledger.utils;
+import android.app.Activity;
+import android.content.Context;
import android.support.annotation.ColorInt;
+import android.view.View;
+import android.view.inputmethod.InputMethodManager;
public final class Globals {
@ColorInt
public static int table_row_odd_bg;
@ColorInt
public static int primaryDark, defaultTextColor;
+ public static void hideSoftKeyboard(Activity act) {
+ // hide the keyboard
+ View v = act.getCurrentFocus();
+ if (v != null) {
+ InputMethodManager imm =
+ (InputMethodManager) act.getSystemService(Context.INPUT_METHOD_SERVICE);
+ imm.hideSoftInputFromWindow(v.getWindowToken(), 0);
+ }
+ }
}
\ No newline at end of file
--- /dev/null
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright © 2018 Damyan Ivanov.
+ ~ This file is part of Mobile-Ledger.
+ ~ Mobile-Ledger is free software: you can distribute it and/or modify it
+ ~ under the term of the GNU General Public License as published by
+ ~ the Free Software Foundation, either version 3 of the License, or
+ ~ (at your opinion), any later version.
+ ~
+ ~ Mobile-Ledger is distributed in the hope that it will be useful,
+ ~ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ ~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ ~ GNU General Public License terms for details.
+ ~
+ ~ You should have received a copy of the GNU General Public License
+ ~ along with Mobile-Ledger. If not, see <https://www.gnu.org/licenses/>.
+ -->
+
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:id="@+id/account_summary_frame"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ tools:context=".ui.account_summary.AccountSummaryFragment">
+
+ <android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:id="@+id/content_account_summary_layout"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ app:layout_behavior="@string/appbar_scrolling_view_behavior"
+ tools:context=".ui.activity.MainActivity"
+ tools:showIn="@layout/app_bar_account_summary">
+
+ <android.support.v4.widget.SwipeRefreshLayout
+ android:id="@+id/account_swiper"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <android.support.v7.widget.RecyclerView
+ android:id="@+id/account_root"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:choiceMode="multipleChoice"
+ android:drawSelectorOnTop="true"
+ android:orientation="vertical">
+
+ </android.support.v7.widget.RecyclerView>
+ </android.support.v4.widget.SwipeRefreshLayout>
+ </android.support.constraint.ConstraintLayout>
+</FrameLayout>
\ No newline at end of file
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
- tools:context=".NewTransactionActivity">
+ tools:context=".ui.activity.NewTransactionActivity">
<android.support.design.widget.AppBarLayout
android:layout_width="match_parent"
-<?xml version="1.0" encoding="utf-8"?>
-<!--
+<?xml version="1.0" encoding="utf-8"?><!--
~ Copyright © 2018 Damyan Ivanov.
~ This file is part of Mobile-Ledger.
~ Mobile-Ledger is free software: you can distribute it and/or modify it
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
- tools:context=".AccountSummary">
+ tools:context=".ui.activity.MainActivity">
<android.support.design.widget.AppBarLayout
android:layout_width="match_parent"
</android.support.design.widget.AppBarLayout>
- <include layout="@layout/content_account_summary" />
+ <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:id="@+id/root_frame"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:layout_marginTop="?attr/actionBarSize"
+ tools:context=".ui.account_summary.AccountSummaryFragment"></FrameLayout>
<android.support.design.widget.FloatingActionButton
android:id="@+id/btn_add_transaction"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior"
- tools:context=".AccountSummary"
+ tools:context=".ui.activity.MainActivity"
tools:showIn="@layout/app_bar_account_summary">
<android.support.v4.widget.SwipeRefreshLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior"
- tools:context=".NewTransactionActivity"
+ tools:context=".ui.activity.NewTransactionActivity"
tools:showIn="@layout/activity_new_transaction">
<ScrollView
android:id="@+id/nav_account_summary"
style="@style/nav_button"
android:drawableStart="@drawable/ic_home_black_24dp"
+ android:onClick="onAccountSummaryClicked"
android:text="@string/account_summary_title" />
<TextView
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
- tools:context=".TransactionListActivity">
+ tools:context=".ui.activity.MainActivity">
<android.support.design.widget.AppBarLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior"
app:layout_constraintBottom_toBottomOf="parent"
- tools:context=".TransactionListActivity">
+ tools:context="net.ktnx.mobileledger.ui.activity.MainActivity">
<LinearLayout
android:id="@+id/last_update_row"
android:layout_weight="1" />
<TextView
- android:id="@+id/textView3"
+ android:id="@+id/clearAccountNameFilter"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/ic_clear_black_24dp"
android:clickable="true"
- android:onClick="onClearAccountNameClick" />
+ android:onClick="onViewClicked"
+ android:focusable="true" />
</LinearLayout>
-->
<menu xmlns:app="http://schemas.android.com/apk/res-auto"
- xmlns:android="http://schemas.android.com/apk/res/android">
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ tools:context="net.ktnx.mobileledger.ui.activity.MainActivity">
<item
android:id="@+id/menu_acc_summary_only_starred"
android:checkable="true"
android:checked="false"
+ android:onClick="onOptionsMenuClicked"
android:title="@string/menu_acc_summary_show_only_starred_title"
app:actionLayout="@layout/switch_item"
- android:onClick="onShowOnlyStarredClicked"
app:showAsAction="never" />
<item android:id="@+id/menu_acc_summary_hide_selected"
android:icon="@drawable/ic_star_white_24dp"
android:title="@string/menu_acc_summary_hide_selected_title"
app:showAsAction="always"
android:visible="false"
+ android:onClick="onOptionsMenuClicked"
/>
<item android:id="@+id/menu_acc_summary_cancel_selection"
android:title="@string/menu_acc_summary_cancel_selection_title"
app:showAsAction="always"
android:visible="false"
- android:onClick="onCancelAccSelection"
+ android:onClick="onOptionsMenuClicked"
android:icon="@drawable/ic_cancel_white_24dp" />
<item android:id="@+id/menu_acc_summary_confirm_selection"
android:title="@string/menu_acc_summary_confirm_selection_title"
app:showAsAction="always"
android:visible="false"
- android:onClick="onConfirmAccSelection"
+ android:onClick="onOptionsMenuClicked"
android:icon="@drawable/ic_check_white_24dp" />
</menu>
\ No newline at end of file
-->
<menu xmlns:app="http://schemas.android.com/apk/res-auto"
- xmlns:android="http://schemas.android.com/apk/res/android">
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools">
<item
+ tools:context="net.ktnx.mobileledger.ui.activity.MainActivity"
android:id="@+id/menu_transaction_list_filter"
android:icon="@drawable/ic_filter_list_white_24dp"
- android:onClick="onShowFilterClick"
+ android:onClick="onOptionsMenuClicked"
android:title="Filter"
app:showAsAction="always" />
</menu>
\ No newline at end of file
<!-- These settings headers are only used on tablets. -->
<header
- android:fragment="net.ktnx.mobileledger.SettingsActivity$BackendPreferenceFragment"
+ android:fragment="net.ktnx.mobileledger.ui.activity.SettingsActivity$BackendPreferenceFragment"
android:icon="@drawable/ic_info_black_24dp"
android:title="@string/pref_header_backend" />
<header
- android:fragment="net.ktnx.mobileledger.SettingsActivity$InterfacePreferenceFragment"
+ android:fragment="net.ktnx.mobileledger.ui.activity.SettingsActivity$InterfacePreferenceFragment"
android:icon="@drawable/ic_info_black_24dp"
android:title="@string/interface_pref_header_title" />
<!--
<header
- android:fragment="net.ktnx.mobileledger.SettingsActivity$NotificationPreferenceFragment"
+ android:fragment="net.ktnx.mobileledger.ui.activity.SettingsActivity$NotificationPreferenceFragment"
android:icon="@drawable/ic_notifications_black_24dp"
android:title="@string/pref_header_notifications" />
<header
- android:fragment="net.ktnx.mobileledger.SettingsActivity$DataSyncPreferenceFragment"
+ android:fragment="net.ktnx.mobileledger.ui.activity.SettingsActivity$DataSyncPreferenceFragment"
android:icon="@drawable/ic_sync_black_24dp"
android:title="@string/pref_header_data_sync" />
-->