Java tutorial
/* * Copyright (c) 2014 - 2016 Ngewi Fet <ngewif@gmail.com> * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.gnucash.android.ui.transaction; import android.app.Activity; import android.content.Intent; import android.content.res.Configuration; import android.database.Cursor; import android.inputmethodservice.KeyboardView; import android.os.Bundle; import android.support.v4.app.Fragment; import android.support.v4.widget.SimpleCursorAdapter; import android.support.v7.app.ActionBar; import android.support.v7.app.AppCompatActivity; import android.text.Editable; import android.text.TextWatcher; 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.widget.AdapterView; import android.widget.CompoundButton; import android.widget.EditText; import android.widget.ImageView; import android.widget.LinearLayout; import android.widget.Spinner; import android.widget.TextView; import android.widget.Toast; import net.objecthunter.exp4j.Expression; import net.objecthunter.exp4j.ExpressionBuilder; import org.gnucash.android.R; import org.gnucash.android.db.DatabaseSchema; import org.gnucash.android.db.adapter.AccountsDbAdapter; import org.gnucash.android.db.adapter.CommoditiesDbAdapter; import org.gnucash.android.model.AccountType; import org.gnucash.android.model.BaseModel; import org.gnucash.android.model.Commodity; import org.gnucash.android.model.Money; import org.gnucash.android.model.Split; import org.gnucash.android.model.Transaction; import org.gnucash.android.model.TransactionType; import org.gnucash.android.ui.common.FormActivity; import org.gnucash.android.ui.common.UxArgument; import org.gnucash.android.ui.transaction.dialog.TransferFundsDialogFragment; import org.gnucash.android.ui.util.widget.CalculatorEditText; import org.gnucash.android.ui.util.widget.CalculatorKeyboard; import org.gnucash.android.ui.util.widget.TransactionTypeSwitch; import org.gnucash.android.util.QualifiedAccountNameCursorAdapter; import java.math.BigDecimal; import java.util.ArrayList; import java.util.List; import butterknife.BindView; import butterknife.ButterKnife; /** * Dialog for editing the splits in a transaction * * @author Ngewi Fet <ngewif@gmail.com> */ public class SplitEditorFragment extends Fragment { @BindView(R.id.split_list_layout) LinearLayout mSplitsLinearLayout; @BindView(R.id.calculator_keyboard) KeyboardView mKeyboardView; @BindView(R.id.imbalance_textview) TextView mImbalanceTextView; private AccountsDbAdapter mAccountsDbAdapter; private Cursor mCursor; private SimpleCursorAdapter mCursorAdapter; private List<View> mSplitItemViewList; private String mAccountUID; private Commodity mCommodity; private BigDecimal mBaseAmount = BigDecimal.ZERO; CalculatorKeyboard mCalculatorKeyboard; BalanceTextWatcher mImbalanceWatcher = new BalanceTextWatcher(); /** * Create and return a new instance of the fragment with the appropriate paramenters * @param args Arguments to be set to the fragment. <br> * See {@link UxArgument#AMOUNT_STRING} and {@link UxArgument#SPLIT_LIST} * @return New instance of SplitEditorFragment */ public static SplitEditorFragment newInstance(Bundle args) { SplitEditorFragment fragment = new SplitEditorFragment(); fragment.setArguments(args); return fragment; } @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View view = inflater.inflate(R.layout.fragment_split_editor, container, false); ButterKnife.bind(this, view); return view; } @Override public void onActivityCreated(Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); ActionBar actionBar = ((AppCompatActivity) getActivity()).getSupportActionBar(); assert actionBar != null; actionBar.setTitle(R.string.title_split_editor); setHasOptionsMenu(true); mCalculatorKeyboard = new CalculatorKeyboard(getActivity(), mKeyboardView, R.xml.calculator_keyboard); mSplitItemViewList = new ArrayList<>(); //we are editing splits for a new transaction. // But the user may have already created some splits before. Let's check List<Split> splitList = getArguments().getParcelableArrayList(UxArgument.SPLIT_LIST); assert splitList != null; initArgs(); if (!splitList.isEmpty()) { //aha! there are some splits. Let's load those instead loadSplitViews(splitList); mImbalanceWatcher.afterTextChanged(null); } else { final String currencyCode = mAccountsDbAdapter.getAccountCurrencyCode(mAccountUID); Split split = new Split(new Money(mBaseAmount.abs(), Commodity.getInstance(currencyCode)), mAccountUID); AccountType accountType = mAccountsDbAdapter.getAccountType(mAccountUID); TransactionType transactionType = Transaction.getTypeForBalance(accountType, mBaseAmount.signum() < 0); split.setType(transactionType); View view = addSplitView(split); view.findViewById(R.id.input_accounts_spinner).setEnabled(false); view.findViewById(R.id.btn_remove_split).setVisibility(View.GONE); TransactionsActivity.displayBalance(mImbalanceTextView, new Money(mBaseAmount.negate(), mCommodity)); } } @Override public void onConfigurationChanged(Configuration newConfig) { super.onConfigurationChanged(newConfig); mCalculatorKeyboard = new CalculatorKeyboard(getActivity(), mKeyboardView, R.xml.calculator_keyboard); } private void loadSplitViews(List<Split> splitList) { for (Split split : splitList) { addSplitView(split); } } @Override public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { inflater.inflate(R.menu.split_editor_actions, menu); } @Override public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { case android.R.id.home: getActivity().setResult(Activity.RESULT_CANCELED); getActivity().finish(); return true; case R.id.menu_save: saveSplits(); return true; case R.id.menu_add_split: addSplitView(null); return true; default: return super.onOptionsItemSelected(item); } } /** * Add a split view and initialize it with <code>split</code> * @param split Split to initialize the contents to * @return Returns the split view which was added */ private View addSplitView(Split split) { LayoutInflater layoutInflater = getActivity().getLayoutInflater(); View splitView = layoutInflater.inflate(R.layout.item_split_entry, mSplitsLinearLayout, false); mSplitsLinearLayout.addView(splitView, 0); SplitViewHolder viewHolder = new SplitViewHolder(splitView, split); splitView.setTag(viewHolder); mSplitItemViewList.add(splitView); return splitView; } /** * Extracts arguments passed to the view and initializes necessary adapters and cursors */ private void initArgs() { mAccountsDbAdapter = AccountsDbAdapter.getInstance(); Bundle args = getArguments(); mAccountUID = ((FormActivity) getActivity()).getCurrentAccountUID(); mBaseAmount = new BigDecimal(args.getString(UxArgument.AMOUNT_STRING)); String conditions = "(" + DatabaseSchema.AccountEntry.COLUMN_HIDDEN + " = 0 AND " + DatabaseSchema.AccountEntry.COLUMN_PLACEHOLDER + " = 0" + ")"; mCursor = mAccountsDbAdapter.fetchAccountsOrderedByFullName(conditions, null); mCommodity = CommoditiesDbAdapter.getInstance() .getCommodity(mAccountsDbAdapter.getCurrencyCode(mAccountUID)); } /** * Holds a split item view and binds the items in it */ class SplitViewHolder implements OnTransferFundsListener { @BindView(R.id.input_split_memo) EditText splitMemoEditText; @BindView(R.id.input_split_amount) CalculatorEditText splitAmountEditText; @BindView(R.id.btn_remove_split) ImageView removeSplitButton; @BindView(R.id.input_accounts_spinner) Spinner accountsSpinner; @BindView(R.id.split_currency_symbol) TextView splitCurrencyTextView; @BindView(R.id.split_uid) TextView splitUidTextView; @BindView(R.id.btn_split_type) TransactionTypeSwitch splitTypeSwitch; View splitView; Money quantity; public SplitViewHolder(View splitView, Split split) { ButterKnife.bind(this, splitView); this.splitView = splitView; if (split != null && !split.getQuantity().equals(split.getValue())) this.quantity = split.getQuantity(); setListeners(split); } @Override public void transferComplete(Money amount) { quantity = amount; } private void setListeners(Split split) { splitAmountEditText.bindListeners(mCalculatorKeyboard); removeSplitButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { mSplitsLinearLayout.removeView(splitView); mSplitItemViewList.remove(splitView); mImbalanceWatcher.afterTextChanged(null); } }); updateTransferAccountsList(accountsSpinner); splitCurrencyTextView.setText(mCommodity.getSymbol()); splitTypeSwitch.setAmountFormattingListener(splitAmountEditText, splitCurrencyTextView); splitTypeSwitch.setChecked(mBaseAmount.signum() > 0); splitUidTextView.setText(BaseModel.generateUID()); if (split != null) { splitAmountEditText.setCommodity(split.getValue().getCommodity()); splitAmountEditText.setValue(split.getFormattedValue().asBigDecimal()); splitCurrencyTextView.setText(split.getValue().getCommodity().getSymbol()); splitMemoEditText.setText(split.getMemo()); splitUidTextView.setText(split.getUID()); String splitAccountUID = split.getAccountUID(); setSelectedTransferAccount(mAccountsDbAdapter.getID(splitAccountUID), accountsSpinner); splitTypeSwitch.setAccountType(mAccountsDbAdapter.getAccountType(splitAccountUID)); splitTypeSwitch.setChecked(split.getType()); } accountsSpinner.setOnItemSelectedListener(new SplitAccountListener(splitTypeSwitch, this)); splitTypeSwitch.addOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() { @Override public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { mImbalanceWatcher.afterTextChanged(null); } }); splitAmountEditText.addTextChangedListener(mImbalanceWatcher); } /** * Returns the value of the amount in the splitAmountEditText field without setting the value to the view * <p>If the expression in the view is currently incomplete or invalid, null is returned. * This method is used primarily for computing the imbalance</p> * @return Value in the split item amount field, or {@link BigDecimal#ZERO} if the expression is empty or invalid */ public BigDecimal getAmountValue() { String amountString = splitAmountEditText.getCleanString(); if (amountString.isEmpty()) return BigDecimal.ZERO; ExpressionBuilder expressionBuilder = new ExpressionBuilder(amountString); Expression expression; try { expression = expressionBuilder.build(); } catch (RuntimeException e) { return BigDecimal.ZERO; } if (expression != null && expression.validate().isValid()) { return new BigDecimal(expression.evaluate()); } else { Log.v(SplitEditorFragment.this.getClass().getSimpleName(), "Incomplete expression for updating imbalance: " + expression); return BigDecimal.ZERO; } } } /** * Updates the spinner to the selected transfer account * @param accountId Database ID of the transfer account */ private void setSelectedTransferAccount(long accountId, final Spinner accountsSpinner) { for (int pos = 0; pos < mCursorAdapter.getCount(); pos++) { if (mCursorAdapter.getItemId(pos) == accountId) { accountsSpinner.setSelection(pos); break; } } } /** * Updates the list of possible transfer accounts. * Only accounts with the same currency can be transferred to */ private void updateTransferAccountsList(Spinner transferAccountSpinner) { mCursorAdapter = new QualifiedAccountNameCursorAdapter(getActivity(), mCursor); transferAccountSpinner.setAdapter(mCursorAdapter); } /** * Check if all the split amounts have valid values that can be saved * @return {@code true} if splits can be saved, {@code false} otherwise */ private boolean canSave() { for (View splitView : mSplitItemViewList) { SplitViewHolder viewHolder = (SplitViewHolder) splitView.getTag(); viewHolder.splitAmountEditText.evaluate(); if (viewHolder.splitAmountEditText.getError() != null) { return false; } //TODO: also check that multicurrency splits have a conversion amount present } return true; } /** * Save all the splits from the split editor */ private void saveSplits() { if (!canSave()) { Toast.makeText(getActivity(), R.string.toast_error_check_split_amounts, Toast.LENGTH_SHORT).show(); return; } Intent data = new Intent(); data.putParcelableArrayListExtra(UxArgument.SPLIT_LIST, extractSplitsFromView()); getActivity().setResult(Activity.RESULT_OK, data); getActivity().finish(); } /** * Extracts the input from the views and builds {@link org.gnucash.android.model.Split}s to correspond to the input. * @return List of {@link org.gnucash.android.model.Split}s represented in the view */ private ArrayList<Split> extractSplitsFromView() { ArrayList<Split> splitList = new ArrayList<>(); for (View splitView : mSplitItemViewList) { SplitViewHolder viewHolder = (SplitViewHolder) splitView.getTag(); if (viewHolder.splitAmountEditText.getValue() == null) continue; BigDecimal amountBigDecimal = viewHolder.splitAmountEditText.getValue(); String currencyCode = mAccountsDbAdapter.getCurrencyCode(mAccountUID); Money valueAmount = new Money(amountBigDecimal.abs(), Commodity.getInstance(currencyCode)); String accountUID = mAccountsDbAdapter.getUID(viewHolder.accountsSpinner.getSelectedItemId()); Split split = new Split(valueAmount, accountUID); split.setMemo(viewHolder.splitMemoEditText.getText().toString()); split.setType(viewHolder.splitTypeSwitch.getTransactionType()); split.setUID(viewHolder.splitUidTextView.getText().toString().trim()); if (viewHolder.quantity != null) split.setQuantity(viewHolder.quantity.abs()); splitList.add(split); } return splitList; } /** * Updates the displayed balance of the accounts when the amount of a split is changed */ private class BalanceTextWatcher implements TextWatcher { @Override public void beforeTextChanged(CharSequence charSequence, int i, int i2, int i3) { //nothing to see here, move along } @Override public void onTextChanged(CharSequence charSequence, int i, int i2, int i3) { //nothing to see here, move along } @Override public void afterTextChanged(Editable editable) { BigDecimal imbalance = BigDecimal.ZERO; for (View splitItem : mSplitItemViewList) { SplitViewHolder viewHolder = (SplitViewHolder) splitItem.getTag(); BigDecimal amount = viewHolder.getAmountValue().abs(); long accountId = viewHolder.accountsSpinner.getSelectedItemId(); boolean hasDebitNormalBalance = AccountsDbAdapter.getInstance().getAccountType(accountId) .hasDebitNormalBalance(); if (viewHolder.splitTypeSwitch.isChecked()) { if (hasDebitNormalBalance) imbalance = imbalance.add(amount); else imbalance = imbalance.subtract(amount); } else { if (hasDebitNormalBalance) imbalance = imbalance.subtract(amount); else imbalance = imbalance.add(amount); } } TransactionsActivity.displayBalance(mImbalanceTextView, new Money(imbalance, mCommodity)); } } /** * Listens to changes in the transfer account and updates the currency symbol, the label of the * transaction type and if neccessary */ private class SplitAccountListener implements AdapterView.OnItemSelectedListener { TransactionTypeSwitch mTypeToggleButton; SplitViewHolder mSplitViewHolder; /** * Flag to know when account spinner callback is due to user interaction or layout of components */ boolean userInteraction = false; public SplitAccountListener(TransactionTypeSwitch typeToggleButton, SplitViewHolder viewHolder) { this.mTypeToggleButton = typeToggleButton; this.mSplitViewHolder = viewHolder; } @Override public void onItemSelected(AdapterView<?> parentView, View selectedItemView, int position, long id) { AccountType accountType = mAccountsDbAdapter.getAccountType(id); mTypeToggleButton.setAccountType(accountType); //refresh the imbalance amount if we change the account mImbalanceWatcher.afterTextChanged(null); String fromCurrencyCode = mAccountsDbAdapter.getCurrencyCode(mAccountUID); String targetCurrencyCode = mAccountsDbAdapter.getCurrencyCode(mAccountsDbAdapter.getUID(id)); if (!userInteraction || fromCurrencyCode.equals(targetCurrencyCode)) { //first call is on layout, subsequent calls will be true and transfer will work as usual userInteraction = true; return; } BigDecimal amountBigD = mSplitViewHolder.splitAmountEditText.getValue(); if (amountBigD == null) return; Money amount = new Money(amountBigD, Commodity.getInstance(fromCurrencyCode)); TransferFundsDialogFragment fragment = TransferFundsDialogFragment.getInstance(amount, targetCurrencyCode, mSplitViewHolder); fragment.show(getFragmentManager(), "tranfer_funds_editor"); } @Override public void onNothingSelected(AdapterView<?> adapterView) { //nothing to see here, move along } } }