Java tutorial
/* * Copyright (c) 2012 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.transactions; import java.math.BigDecimal; import java.math.RoundingMode; import java.text.DecimalFormat; import java.text.NumberFormat; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.Calendar; import java.util.Currency; import java.util.Date; import java.util.GregorianCalendar; import java.util.Locale; import org.gnucash.android.R; import org.gnucash.android.data.Money; import org.gnucash.android.data.Transaction; import org.gnucash.android.data.Transaction.TransactionType; import org.gnucash.android.db.AccountsDbAdapter; import org.gnucash.android.db.TransactionsDbAdapter; import org.gnucash.android.ui.DatePickerDialogFragment; import org.gnucash.android.ui.TimePickerDialogFragment; import org.gnucash.android.ui.widget.WidgetConfigurationActivity; import android.app.DatePickerDialog; import android.app.DatePickerDialog.OnDateSetListener; import android.app.TimePickerDialog; import android.app.TimePickerDialog.OnTimeSetListener; import android.content.Context; import android.os.Bundle; import android.preference.PreferenceManager; import android.support.v4.app.DialogFragment; import android.support.v4.app.FragmentTransaction; import android.text.Editable; import android.text.TextWatcher; import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.view.inputmethod.InputMethodManager; import android.widget.CompoundButton; import android.widget.CompoundButton.OnCheckedChangeListener; import android.widget.DatePicker; import android.widget.EditText; import android.widget.TextView; import android.widget.TimePicker; import android.widget.ToggleButton; import com.actionbarsherlock.app.ActionBar; import com.actionbarsherlock.app.SherlockFragment; import com.actionbarsherlock.view.Menu; import com.actionbarsherlock.view.MenuInflater; import com.actionbarsherlock.view.MenuItem; /** * Fragment for creating or editing transactions * @author Ngewi Fet <ngewif@gmail.com> */ public class NewTransactionFragment extends SherlockFragment implements OnDateSetListener, OnTimeSetListener { /** * Transactions database adapter */ private TransactionsDbAdapter mTransactionsDbAdapter; /** * Holds database ID of transaction to be edited (if in edit mode) */ private long mTransactionId = 0; /** * Transaction to be created/updated */ private Transaction mTransaction; /** * Arguments key for database ID of transaction. * Is used to pass a transaction ID into a bundle or intent */ public static final String SELECTED_TRANSACTION_ID = "selected_transaction_id"; /** * Formats a {@link Date} object into a date string of the format dd MMM yyyy e.g. 18 July 2012 */ public final static SimpleDateFormat DATE_FORMATTER = new SimpleDateFormat("dd MMM yyyy"); /** * Formats a {@link Date} object to time string of format HH:mm e.g. 15:25 */ public final static SimpleDateFormat TIME_FORMATTER = new SimpleDateFormat("HH:mm"); /** * Button for setting the transaction type, either credit or debit */ private ToggleButton mTransactionTypeButton; /** * Input field for the transaction name (description) */ private EditText mNameEditText; /** * Input field for the transaction amount */ private EditText mAmountEditText; /** * Field for the transaction currency. * The transaction uses the currency of the account */ private TextView mCurrencyTextView; /** * Input field for the transaction description (note) */ private EditText mDescriptionEditText; /** * Input field for the transaction date */ private TextView mDateTextView; /** * Input field for the transaction time */ private TextView mTimeTextView; /** * {@link Calendar} for holding the set date */ private Calendar mDate; /** * {@link Calendar} object holding the set time */ private Calendar mTime; /** * ActionBar Menu item for saving the transaction * A transaction needs atleast a name and amount, only then is the save menu item enabled */ private MenuItem mSaveMenuItem; /** * Create the view and retrieve references to the UI elements */ @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View v = inflater.inflate(R.layout.fragment_new_transaction, container, false); mNameEditText = (EditText) v.findViewById(R.id.input_transaction_name); mDescriptionEditText = (EditText) v.findViewById(R.id.input_description); mDateTextView = (TextView) v.findViewById(R.id.input_date); mTimeTextView = (TextView) v.findViewById(R.id.input_time); mAmountEditText = (EditText) v.findViewById(R.id.input_transaction_amount); mCurrencyTextView = (TextView) v.findViewById(R.id.currency_symbol); mTransactionTypeButton = (ToggleButton) v.findViewById(R.id.input_transaction_type); return v; } @Override public void onActivityCreated(Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); setHasOptionsMenu(true); ActionBar actionBar = getSherlockActivity().getSupportActionBar(); actionBar.setHomeButtonEnabled(true); actionBar.setDisplayHomeAsUpEnabled(true); actionBar.setDisplayShowTitleEnabled(false); mTransactionId = getArguments().getLong(SELECTED_TRANSACTION_ID); mTransactionsDbAdapter = new TransactionsDbAdapter(getActivity()); mTransaction = mTransactionsDbAdapter.getTransaction(mTransactionId); setListeners(); if (mTransaction == null) initalizeViews(); else initializeViewsWithTransaction(); } /** * Initialize views in the fragment with information from a transaction. * This method is called if the fragment is used for editing a transaction */ private void initializeViewsWithTransaction() { mNameEditText.setText(mTransaction.getName()); mTransactionTypeButton.setChecked(mTransaction.getTransactionType() == TransactionType.DEBIT); mAmountEditText.setText(mTransaction.getAmount().toPlainString()); mCurrencyTextView.setText(mTransaction.getAmount().getCurrency().getSymbol(Locale.getDefault())); mDescriptionEditText.setText(mTransaction.getDescription()); mDateTextView.setText(DATE_FORMATTER.format(mTransaction.getTimeMillis())); mTimeTextView.setText(TIME_FORMATTER.format(mTransaction.getTimeMillis())); Calendar cal = GregorianCalendar.getInstance(); cal.setTimeInMillis(mTransaction.getTimeMillis()); mDate = mTime = cal; final long accountId = mTransactionsDbAdapter.getAccountID(mTransaction.getAccountUID()); String code = mTransactionsDbAdapter.getCurrencyCode(accountId); Currency accountCurrency = Currency.getInstance(code); mCurrencyTextView.setText(accountCurrency.getSymbol()); } /** * Initialize views with default data for new transactions */ private void initalizeViews() { Date time = new Date(System.currentTimeMillis()); mDateTextView.setText(DATE_FORMATTER.format(time)); mTimeTextView.setText(TIME_FORMATTER.format(time)); mTime = mDate = Calendar.getInstance(); String typePref = PreferenceManager.getDefaultSharedPreferences(getActivity()) .getString(getString(R.string.key_default_transaction_type), "DEBIT"); if (typePref.equals("CREDIT")) { mTransactionTypeButton.setChecked(false); } final long accountId = getArguments().getLong(TransactionsListFragment.SELECTED_ACCOUNT_ID); String code = Money.DEFAULT_CURRENCY_CODE; if (accountId != 0) code = mTransactionsDbAdapter.getCurrencyCode(accountId); Currency accountCurrency = Currency.getInstance(code); mCurrencyTextView.setText(accountCurrency.getSymbol(Locale.getDefault())); } /** * Sets click listeners for the dialog buttons */ private void setListeners() { ValidationsWatcher validations = new ValidationsWatcher(); mAmountEditText.addTextChangedListener(validations); mNameEditText.addTextChangedListener(validations); mAmountEditText.addTextChangedListener(new AmountInputFormatter()); mTransactionTypeButton.setOnCheckedChangeListener(new OnCheckedChangeListener() { @Override public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { if (isChecked) { int red = getResources().getColor(R.color.debit_red); mTransactionTypeButton.setTextColor(red); mAmountEditText.setTextColor(red); mCurrencyTextView.setTextColor(red); } else { int green = getResources().getColor(R.color.credit_green); mTransactionTypeButton.setTextColor(green); mAmountEditText.setTextColor(green); mCurrencyTextView.setTextColor(green); } String amountText = mAmountEditText.getText().toString(); if (amountText.length() > 0) { Money money = new Money(stripCurrencyFormatting(amountText)).divide(100).negate(); mAmountEditText.setText(money.toPlainString()); //trigger an edit to update the number sign } } }); mDateTextView.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { FragmentTransaction ft = getFragmentManager().beginTransaction(); long dateMillis = 0; try { Date date = DATE_FORMATTER.parse(mDateTextView.getText().toString()); dateMillis = date.getTime(); } catch (ParseException e) { Log.e(getTag(), "Error converting input time to Date object"); } DialogFragment newFragment = new DatePickerDialogFragment(NewTransactionFragment.this, dateMillis); newFragment.show(ft, "date_dialog"); } }); mTimeTextView.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { FragmentTransaction ft = getFragmentManager().beginTransaction(); long timeMillis = 0; try { Date date = TIME_FORMATTER.parse(mTimeTextView.getText().toString()); timeMillis = date.getTime(); } catch (ParseException e) { Log.e(getTag(), "Error converting input time to Date object"); } DialogFragment fragment = new TimePickerDialogFragment(NewTransactionFragment.this, timeMillis); fragment.show(ft, "time_dialog"); } }); } public void onAccountChanged(long newAccountId) { AccountsDbAdapter accountsDbAdapter = new AccountsDbAdapter(getActivity()); String currencyCode = accountsDbAdapter.getCurrencyCode(newAccountId); Currency currency = Currency.getInstance(currencyCode); mCurrencyTextView.setText(currency.getSymbol(Locale.getDefault())); accountsDbAdapter.close(); } /** * Collects information from the fragment views and uses it to create * and save a transaction */ private void saveNewTransaction() { Calendar cal = new GregorianCalendar(mDate.get(Calendar.YEAR), mDate.get(Calendar.MONTH), mDate.get(Calendar.DAY_OF_MONTH), mTime.get(Calendar.HOUR_OF_DAY), mTime.get(Calendar.MINUTE), mTime.get(Calendar.SECOND)); String name = mNameEditText.getText().toString(); String description = mDescriptionEditText.getText().toString(); BigDecimal amountBigd = parseInputToDecimal(mAmountEditText.getText().toString()); long accountID = ((TransactionsActivity) getSherlockActivity()).getCurrentAccountID(); //mAccountsSpinner.getSelectedItemId(); Currency currency = Currency.getInstance(mTransactionsDbAdapter.getCurrencyCode(accountID)); Money amount = new Money(amountBigd, currency); TransactionType type = mTransactionTypeButton.isChecked() ? TransactionType.DEBIT : TransactionType.CREDIT; if (mTransaction != null) { mTransaction.setAmount(amount); mTransaction.setName(name); mTransaction.setTransactionType(type); } else { mTransaction = new Transaction(amount, name, type); } mTransaction.setAccountUID(mTransactionsDbAdapter.getAccountUID(accountID)); mTransaction.setTime(cal.getTimeInMillis()); mTransaction.setDescription(description); mTransactionsDbAdapter.addTransaction(mTransaction); mTransactionsDbAdapter.close(); //update widgets, if any WidgetConfigurationActivity.updateAllWidgets(getActivity().getApplicationContext()); finish(); } @Override public void onDestroyView() { super.onDestroyView(); mTransactionsDbAdapter.close(); } @Override public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { inflater.inflate(R.menu.new_transaction_actions, menu); mSaveMenuItem = menu.findItem(R.id.menu_save); //only initially enable if we are editing a transaction mSaveMenuItem.setEnabled(mTransactionId > 0); } @Override public boolean onOptionsItemSelected(MenuItem item) { //hide the keyboard if it is visible InputMethodManager imm = (InputMethodManager) getActivity().getSystemService(Context.INPUT_METHOD_SERVICE); imm.hideSoftInputFromWindow(mNameEditText.getApplicationWindowToken(), 0); switch (item.getItemId()) { case R.id.menu_cancel: finish(); return true; case R.id.menu_save: saveNewTransaction(); return true; default: return false; } } /** * Finishes the fragment appropriately. * Depends on how the fragment was loaded, it might have a backstack or not */ private void finish() { if (getActivity().getSupportFragmentManager().getBackStackEntryCount() == 0) { //means we got here directly from the accounts list activity, need to finish getActivity().finish(); } else { //go back to transactions list getSherlockActivity().getSupportFragmentManager().popBackStack(); } } /** * Callback when the date is set in the {@link DatePickerDialog} */ @Override public void onDateSet(DatePicker view, int year, int monthOfYear, int dayOfMonth) { Calendar cal = new GregorianCalendar(year, monthOfYear, dayOfMonth); mDateTextView.setText(DATE_FORMATTER.format(cal.getTime())); mDate.set(Calendar.YEAR, year); mDate.set(Calendar.MONTH, monthOfYear); mDate.set(Calendar.DAY_OF_MONTH, dayOfMonth); } /** * Callback when the time is set in the {@link TimePickerDialog} */ @Override public void onTimeSet(TimePicker view, int hourOfDay, int minute) { Calendar cal = new GregorianCalendar(0, 0, 0, hourOfDay, minute); mTimeTextView.setText(TIME_FORMATTER.format(cal.getTime())); mTime.set(Calendar.HOUR_OF_DAY, hourOfDay); mTime.set(Calendar.MINUTE, minute); } /** * Strips formatting from a currency string. * All non-digit information is removed * @param s String to be stripped * @return Stripped string with all non-digits removed */ public static String stripCurrencyFormatting(String s) { //remove all currency formatting and anything else which is not a number return s.trim().replaceAll("\\D*", ""); } /** * Parse an input string into a {@link BigDecimal} * @param amountString String with amount information * @return BigDecimal with the amount parsed from <code>amountString</code> */ public BigDecimal parseInputToDecimal(String amountString) { String clean = stripCurrencyFormatting(amountString); //all amounts are input to 2 decimal places, so after removing decimal separator, divide by 100 BigDecimal amount = new BigDecimal(clean).setScale(2, RoundingMode.HALF_EVEN).divide(new BigDecimal(100), 2, RoundingMode.HALF_EVEN); if (mTransactionTypeButton.isChecked() && amount.doubleValue() > 0) amount = amount.negate(); return amount; } /** * Validates that the name and amount of the transaction is provided * before enabling the save button * @author Ngewi Fet <ngewif@gmail.com> * */ private class ValidationsWatcher implements TextWatcher { @Override public void afterTextChanged(Editable s) { boolean valid = (mAmountEditText.getText().length() > 0); //JellyBean 4.2 calls onActivityCreated before creating the menu if (mSaveMenuItem != null) mSaveMenuItem.setEnabled(valid); } @Override public void beforeTextChanged(CharSequence s, int start, int count, int after) { // TODO Auto-generated method stub } @Override public void onTextChanged(CharSequence s, int start, int before, int count) { // TODO Auto-generated method stub } } /** * Captures input string in the amount input field and parses it into a formatted amount * The amount input field allows numbers to be input sequentially and they are parsed * into a string with 2 decimal places. This means inputting 245 will result in the amount * of 2.45 * @author Ngewi Fet <ngewif@gmail.com> */ private class AmountInputFormatter implements TextWatcher { private String current = "0"; @Override public void afterTextChanged(Editable s) { if (s.length() == 0) return; BigDecimal amount = parseInputToDecimal(s.toString()); DecimalFormat formatter = (DecimalFormat) NumberFormat.getInstance(Locale.getDefault()); formatter.setMinimumFractionDigits(2); formatter.setMaximumFractionDigits(2); current = formatter.format(amount.doubleValue()); mAmountEditText.removeTextChangedListener(this); mAmountEditText.setText(current); mAmountEditText.setSelection(current.length()); mAmountEditText.addTextChangedListener(this); } @Override public void beforeTextChanged(CharSequence s, int start, int count, int after) { // nothing to see here, move along } @Override public void onTextChanged(CharSequence s, int start, int before, int count) { // nothing to see here, move along } } }