From: Damyan Ivanov Date: Mon, 31 Dec 2018 05:31:57 +0000 (+0000) Subject: major refactor, make account summary and transaction list fragments, part of the... X-Git-Tag: v0.3~159 X-Git-Url: https://git.ktnx.net/?p=mobile-ledger.git;a=commitdiff_plain;h=0bbdc409d82da31324c031f36607510f17d992e6 major refactor, make account summary and transaction list fragments, part of the "Main" activity --- diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 1cd74d14..5ab5cdc6 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -31,7 +31,7 @@ android:name="net.ktnx.mobileledger.MobileLedgerApplication" android:theme="@style/AppTheme"> @@ -40,34 +40,21 @@ + android:parentActivityName=".ui.activity.MainActivity"> + android:value="net.ktnx.mobileledger.ui.activity.MainActivity" /> - - - - - - - + android:value="net.ktnx.mobileledger.ui.activity.MainActivity" /> diff --git a/app/src/main/java/net/ktnx/mobileledger/AccountSummary.java b/app/src/main/java/net/ktnx/mobileledger/AccountSummary.java deleted file mode 100644 index 3a59003d..00000000 --- a/app/src/main/java/net/ktnx/mobileledger/AccountSummary.java +++ /dev/null @@ -1,311 +0,0 @@ -/* - * 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 . - */ - -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 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(); - } -} diff --git a/app/src/main/java/net/ktnx/mobileledger/AccountSummaryViewModel.java b/app/src/main/java/net/ktnx/mobileledger/AccountSummaryViewModel.java deleted file mode 100644 index f9b5d74f..00000000 --- a/app/src/main/java/net/ktnx/mobileledger/AccountSummaryViewModel.java +++ /dev/null @@ -1,211 +0,0 @@ -/* - * 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 . - */ - -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 accounts; - - public AccountSummaryViewModel(@NonNull Application application) { - super(application); - } - - List 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 { - private List accounts; - private boolean selectionActive; - - AccountSummaryAdapter(List 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 diff --git a/app/src/main/java/net/ktnx/mobileledger/AppCompatPreferenceActivity.java b/app/src/main/java/net/ktnx/mobileledger/AppCompatPreferenceActivity.java deleted file mode 100644 index 359bbad6..00000000 --- a/app/src/main/java/net/ktnx/mobileledger/AppCompatPreferenceActivity.java +++ /dev/null @@ -1,128 +0,0 @@ -/* - * 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 . - */ - -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; - } -} diff --git a/app/src/main/java/net/ktnx/mobileledger/NewTransactionActivity.java b/app/src/main/java/net/ktnx/mobileledger/NewTransactionActivity.java deleted file mode 100644 index e7d6ca4b..00000000 --- a/app/src/main/java/net/ktnx/mobileledger/NewTransactionActivity.java +++ /dev/null @@ -1,445 +0,0 @@ -/* - * 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 . - */ - -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(""); - } - } -} diff --git a/app/src/main/java/net/ktnx/mobileledger/OnSwipeTouchListener.java b/app/src/main/java/net/ktnx/mobileledger/OnSwipeTouchListener.java deleted file mode 100644 index 3257b2c4..00000000 --- a/app/src/main/java/net/ktnx/mobileledger/OnSwipeTouchListener.java +++ /dev/null @@ -1,90 +0,0 @@ -/* - * 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 . - */ - -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() {} -} diff --git a/app/src/main/java/net/ktnx/mobileledger/RecyclerItemListener.java b/app/src/main/java/net/ktnx/mobileledger/RecyclerItemListener.java deleted file mode 100644 index b95f4f77..00000000 --- a/app/src/main/java/net/ktnx/mobileledger/RecyclerItemListener.java +++ /dev/null @@ -1,73 +0,0 @@ -/* - * 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 . - */ - -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) { - - } -} diff --git a/app/src/main/java/net/ktnx/mobileledger/SettingsActivity.java b/app/src/main/java/net/ktnx/mobileledger/SettingsActivity.java deleted file mode 100644 index 07e0c61b..00000000 --- a/app/src/main/java/net/ktnx/mobileledger/SettingsActivity.java +++ /dev/null @@ -1,319 +0,0 @@ -/* - * 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 . - */ - -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. - *

- * See - * Android Design: Settings for design guidelines and the Settings - * API Guide 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

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); - } - } -} diff --git a/app/src/main/java/net/ktnx/mobileledger/TransactionListActivity.java b/app/src/main/java/net/ktnx/mobileledger/TransactionListActivity.java deleted file mode 100644 index ac992cdf..00000000 --- a/app/src/main/java/net/ktnx/mobileledger/TransactionListActivity.java +++ /dev/null @@ -1,245 +0,0 @@ -/* - * 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 . - */ - -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); - } -} diff --git a/app/src/main/java/net/ktnx/mobileledger/TransactionListAdapter.java b/app/src/main/java/net/ktnx/mobileledger/TransactionListAdapter.java deleted file mode 100644 index 41c3cdd5..00000000 --- a/app/src/main/java/net/ktnx/mobileledger/TransactionListAdapter.java +++ /dev/null @@ -1,162 +0,0 @@ -/* - * 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 . - */ - -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 { - 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 diff --git a/app/src/main/java/net/ktnx/mobileledger/async/RetrieveAccountsTask.java b/app/src/main/java/net/ktnx/mobileledger/async/RetrieveAccountsTask.java index a342ca4d..59362fad 100644 --- a/app/src/main/java/net/ktnx/mobileledger/async/RetrieveAccountsTask.java +++ b/app/src/main/java/net/ktnx/mobileledger/async/RetrieveAccountsTask.java @@ -21,7 +21,7 @@ import android.content.SharedPreferences; 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; @@ -41,10 +41,10 @@ import java.util.regex.Pattern; public class RetrieveAccountsTask extends android.os.AsyncTask { int error; - WeakReference mContext; + WeakReference mContext; private SharedPreferences pref; - public RetrieveAccountsTask(WeakReference context) { + public RetrieveAccountsTask(WeakReference context) { mContext = context; error = 0; } @@ -57,7 +57,7 @@ public class RetrieveAccountsTask extends android.os.AsyncTask contextRef; + protected WeakReference contextRef; protected int error; private boolean success; - public RetrieveTransactionsTask(WeakReference contextRef) { + public RetrieveTransactionsTask(WeakReference contextRef) { this.contextRef = contextRef; } private static final void L(String msg) { @@ -64,28 +64,28 @@ public class RetrieveTransactionsTask extends @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); } @@ -100,9 +100,9 @@ public class RetrieveTransactionsTask extends 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())); @@ -251,7 +251,8 @@ public class RetrieveTransactionsTask extends 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); } } @@ -269,7 +270,7 @@ public class RetrieveTransactionsTask extends } return null; } - TransactionListActivity getContext() { + TransactionListFragment getContext() { return contextRef.get(); } diff --git a/app/src/main/java/net/ktnx/mobileledger/ui/OnSwipeTouchListener.java b/app/src/main/java/net/ktnx/mobileledger/ui/OnSwipeTouchListener.java new file mode 100644 index 00000000..ecc00959 --- /dev/null +++ b/app/src/main/java/net/ktnx/mobileledger/ui/OnSwipeTouchListener.java @@ -0,0 +1,90 @@ +/* + * 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 . + */ + +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() {} +} diff --git a/app/src/main/java/net/ktnx/mobileledger/ui/RecyclerItemListener.java b/app/src/main/java/net/ktnx/mobileledger/ui/RecyclerItemListener.java new file mode 100644 index 00000000..64427c2e --- /dev/null +++ b/app/src/main/java/net/ktnx/mobileledger/ui/RecyclerItemListener.java @@ -0,0 +1,73 @@ +/* + * 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 . + */ + +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) { + + } +} diff --git a/app/src/main/java/net/ktnx/mobileledger/ui/account_summary/AccountSummaryFragment.java b/app/src/main/java/net/ktnx/mobileledger/ui/account_summary/AccountSummaryFragment.java new file mode 100644 index 00000000..b0b67327 --- /dev/null +++ b/app/src/main/java/net/ktnx/mobileledger/ui/account_summary/AccountSummaryFragment.java @@ -0,0 +1,236 @@ +/* + * 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 . + */ + +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 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"); + } +} diff --git a/app/src/main/java/net/ktnx/mobileledger/ui/account_summary/AccountSummaryViewModel.java b/app/src/main/java/net/ktnx/mobileledger/ui/account_summary/AccountSummaryViewModel.java new file mode 100644 index 00000000..942a5685 --- /dev/null +++ b/app/src/main/java/net/ktnx/mobileledger/ui/account_summary/AccountSummaryViewModel.java @@ -0,0 +1,213 @@ +/* + * 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 . + */ + +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 accounts; + + public AccountSummaryViewModel(@NonNull Application application) { + super(application); + } + + List 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 { + private List accounts; + private boolean selectionActive; + + AccountSummaryAdapter(List 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 diff --git a/app/src/main/java/net/ktnx/mobileledger/ui/activity/AppCompatPreferenceActivity.java b/app/src/main/java/net/ktnx/mobileledger/ui/activity/AppCompatPreferenceActivity.java new file mode 100644 index 00000000..6aeb863a --- /dev/null +++ b/app/src/main/java/net/ktnx/mobileledger/ui/activity/AppCompatPreferenceActivity.java @@ -0,0 +1,128 @@ +/* + * 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 . + */ + +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; + } +} diff --git a/app/src/main/java/net/ktnx/mobileledger/ui/activity/MainActivity.java b/app/src/main/java/net/ktnx/mobileledger/ui/activity/MainActivity.java new file mode 100644 index 00000000..e66cf045 --- /dev/null +++ b/app/src/main/java/net/ktnx/mobileledger/ui/activity/MainActivity.java @@ -0,0 +1,196 @@ +/* + * 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 . + */ + +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(); + } + } + +} diff --git a/app/src/main/java/net/ktnx/mobileledger/ui/activity/NewTransactionActivity.java b/app/src/main/java/net/ktnx/mobileledger/ui/activity/NewTransactionActivity.java new file mode 100644 index 00000000..43c10b27 --- /dev/null +++ b/app/src/main/java/net/ktnx/mobileledger/ui/activity/NewTransactionActivity.java @@ -0,0 +1,447 @@ +/* + * 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 . + */ + +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(""); + } + } +} diff --git a/app/src/main/java/net/ktnx/mobileledger/ui/activity/SettingsActivity.java b/app/src/main/java/net/ktnx/mobileledger/ui/activity/SettingsActivity.java new file mode 100644 index 00000000..8f6ff2fb --- /dev/null +++ b/app/src/main/java/net/ktnx/mobileledger/ui/activity/SettingsActivity.java @@ -0,0 +1,319 @@ +/* + * 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 . + */ + +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. + *

+ * See + * Android Design: Settings for design guidelines and the Settings + * API Guide 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

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); + } + } +} diff --git a/app/src/main/java/net/ktnx/mobileledger/ui/transaction_list/TransactionListAdapter.java b/app/src/main/java/net/ktnx/mobileledger/ui/transaction_list/TransactionListAdapter.java new file mode 100644 index 00000000..7983aa5b --- /dev/null +++ b/app/src/main/java/net/ktnx/mobileledger/ui/transaction_list/TransactionListAdapter.java @@ -0,0 +1,162 @@ +/* + * 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 . + */ + +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 { + 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 diff --git a/app/src/main/java/net/ktnx/mobileledger/ui/transaction_list/TransactionListFragment.java b/app/src/main/java/net/ktnx/mobileledger/ui/transaction_list/TransactionListFragment.java index 97d2fe80..72e07b27 100644 --- a/app/src/main/java/net/ktnx/mobileledger/ui/transaction_list/TransactionListFragment.java +++ b/app/src/main/java/net/ktnx/mobileledger/ui/transaction_list/TransactionListFragment.java @@ -18,22 +18,67 @@ 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 @@ -46,8 +91,158 @@ public class TransactionListFragment extends Fragment { 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); + } } diff --git a/app/src/main/java/net/ktnx/mobileledger/ui/transaction_list/TransactionListViewModel.java b/app/src/main/java/net/ktnx/mobileledger/ui/transaction_list/TransactionListViewModel.java index 1680236b..ec339fcf 100644 --- a/app/src/main/java/net/ktnx/mobileledger/ui/transaction_list/TransactionListViewModel.java +++ b/app/src/main/java/net/ktnx/mobileledger/ui/transaction_list/TransactionListViewModel.java @@ -17,8 +17,8 @@ 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; @@ -26,7 +26,6 @@ import android.view.View; 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; @@ -36,10 +35,11 @@ public class TransactionListViewModel extends ViewModel { private ArrayList transactions; - public void reloadTransactions(Context context) { + public void reloadTransactions(TransactionListFragment context) { ArrayList 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; @@ -64,7 +64,7 @@ public class TransactionListViewModel extends ViewModel { } 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))); diff --git a/app/src/main/java/net/ktnx/mobileledger/utils/Globals.java b/app/src/main/java/net/ktnx/mobileledger/utils/Globals.java index b7fffadf..e32a4321 100644 --- a/app/src/main/java/net/ktnx/mobileledger/utils/Globals.java +++ b/app/src/main/java/net/ktnx/mobileledger/utils/Globals.java @@ -17,7 +17,11 @@ 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 @@ -26,5 +30,14 @@ public final class Globals { 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 diff --git a/app/src/main/res/layout/account_summary_fragment.xml b/app/src/main/res/layout/account_summary_fragment.xml new file mode 100644 index 00000000..72d800c7 --- /dev/null +++ b/app/src/main/res/layout/account_summary_fragment.xml @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_new_transaction.xml b/app/src/main/res/layout/activity_new_transaction.xml index c03c5988..b36b0a03 100644 --- a/app/src/main/res/layout/activity_new_transaction.xml +++ b/app/src/main/res/layout/activity_new_transaction.xml @@ -21,7 +21,7 @@ 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"> - + xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:tools="http://schemas.android.com/tools" + tools:context="net.ktnx.mobileledger.ui.activity.MainActivity"> \ No newline at end of file diff --git a/app/src/main/res/menu/transaction_list.xml b/app/src/main/res/menu/transaction_list.xml index 00433377..80232e4a 100644 --- a/app/src/main/res/menu/transaction_list.xml +++ b/app/src/main/res/menu/transaction_list.xml @@ -17,12 +17,14 @@ --> + xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:tools="http://schemas.android.com/tools"> \ No newline at end of file diff --git a/app/src/main/res/xml/pref_headers.xml b/app/src/main/res/xml/pref_headers.xml index 8054ff3b..de84c0a6 100644 --- a/app/src/main/res/xml/pref_headers.xml +++ b/app/src/main/res/xml/pref_headers.xml @@ -20,21 +20,21 @@