ivl.android.moneybalance.ExpenseEditorActivity.java Source code

Java tutorial

Introduction

Here is the source code for ivl.android.moneybalance.ExpenseEditorActivity.java

Source

/*
 * MoneyBalance - Android-based calculator for tracking and balancing expenses
 * Copyright (C) 2012 Ingo van Lil <inguin@gmx.de>
 *
 * 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 ivl.android.moneybalance;

import ivl.android.moneybalance.dao.CalculationDataSource;
import ivl.android.moneybalance.dao.DataBaseHelper;
import ivl.android.moneybalance.dao.ExpenseDataSource;
import ivl.android.moneybalance.data.Currency;
import ivl.android.moneybalance.data.Calculation;
import ivl.android.moneybalance.data.Expense;
import ivl.android.moneybalance.data.Person;

import java.text.DateFormat;
import java.text.NumberFormat;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import android.app.AlertDialog;
import android.app.DatePickerDialog;
import android.app.Dialog;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.res.Resources;
import android.os.Bundle;
import android.support.v4.app.DialogFragment;
import android.support.v4.view.WindowCompat;
import android.support.v7.app.ActionBarActivity;
import android.text.Editable;
import android.text.TextWatcher;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.widget.AdapterView;
import android.widget.AdapterView.OnItemSelectedListener;
import android.widget.ArrayAdapter;
import android.widget.AutoCompleteTextView;
import android.widget.CheckBox;
import android.widget.CompoundButton;
import android.widget.DatePicker;
import android.widget.EditText;
import android.widget.Spinner;
import android.widget.TableLayout;
import android.widget.TableRow;
import android.widget.TextView;
import android.widget.Toast;

public class ExpenseEditorActivity extends ActionBarActivity {

    public static final String PARAM_EXPENSE_ID = "expenseId";
    public static final String PARAM_PERSON_ID = "personId";
    public static final String PARAM_CALCULATION_ID = "calculationId";
    public static final String PARAM_DATE = "date";

    private enum Mode {
        NEW_EXPENSE, EDIT_EXPENSE
    }

    private Mode mode;
    private Expense expense;

    private final DataBaseHelper dbHelper = new DataBaseHelper(this);

    private final CalculationDataSource calculationDataSource = new CalculationDataSource(dbHelper);
    private ExpenseDataSource expenseDataSource;

    private Calculation calculation;
    private List<Person> persons;

    private AutoCompleteTextView titleView;
    private EditText amountView;
    private Spinner currencySpinner;
    private TextView payerView;
    private TextView dateView;

    private static class CustomSplitEntry {
        CheckBox enabled;
        EditText weight;
        TextView result;
    }

    private CustomSplitEntry[] customSplitEntries;

    private CheckBox customSplitCheckBox;
    private TableLayout customSplitTable;

    private CurrencyHelper currencyHelper;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        supportRequestWindowFeature(WindowCompat.FEATURE_ACTION_BAR);

        setContentView(R.layout.expense_editor);
        titleView = (AutoCompleteTextView) findViewById(R.id.expense_title);
        amountView = (EditText) findViewById(R.id.expense_amount);
        currencySpinner = (Spinner) findViewById(R.id.expense_currency);
        payerView = (TextView) findViewById(R.id.expense_payer);
        dateView = (TextView) findViewById(R.id.expense_date);

        customSplitCheckBox = (CheckBox) findViewById(R.id.custom_split);
        customSplitTable = (TableLayout) findViewById(R.id.expense_split_table);

        Intent intent = getIntent();

        long calculationId = intent.getLongExtra(PARAM_CALCULATION_ID, -1);
        long expenseId = intent.getLongExtra(PARAM_EXPENSE_ID, -1);

        calculation = calculationDataSource.get(calculationId);
        persons = calculation.getPersons();

        expenseDataSource = new ExpenseDataSource(dbHelper, calculation);

        mode = (expenseId >= 0 ? Mode.EDIT_EXPENSE : Mode.NEW_EXPENSE);
        if (mode == Mode.EDIT_EXPENSE) {
            setTitle(R.string.edit_expense);
            expense = expenseDataSource.get(expenseId);
            titleView.setText(expense.getTitle());
        } else {
            setTitle(R.string.new_expense);
            expense = new Expense(calculation);
            long personId = intent.getLongExtra(PARAM_PERSON_ID, -1);
            expense.setPerson(calculation.getPersonById(personId));
            long millis = intent.getLongExtra(PARAM_DATE, -1);
            if (millis > 0) {
                Calendar cal = Calendar.getInstance();
                cal.setTimeInMillis(millis);
                expense.setDate(cal);
            }
        }

        List<java.util.Currency> currencies = new ArrayList<>();
        for (Currency currency : calculation.getCurrencies())
            currencies.add(java.util.Currency.getInstance(currency.getCurrencyCode()));
        CurrencySpinnerAdapter adapter = new CurrencySpinnerAdapter(this, currencies);
        adapter.setSymbolOnly(true);

        Currency currency = expense.getCurrency();
        currencyHelper = currency.getCurrencyHelper();
        currencySpinner.setAdapter(adapter);
        currencySpinner.setSelection(adapter.findItem(currency.getCurrencyCode()));
        currencySpinner.setEnabled(currencies.size() > 1);

        currencySpinner.setOnItemSelectedListener(new OnItemSelectedListener() {
            @Override
            public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
                updateCurrency();
            }

            @Override
            public void onNothingSelected(AdapterView<?> parent) {
            }
        });

        if (mode == Mode.EDIT_EXPENSE) {
            String formatted = currencyHelper.format(expense.getAmount(), false);
            amountView.setText(formatted);
        }

        Set<String> expenseTitles = new HashSet<>();
        for (Expense expense : calculation.getExpenses())
            expenseTitles.add(expense.getTitle());
        ArrayAdapter<String> expenseTitlesAdapter = new ArrayAdapter<>(this,
                android.R.layout.simple_dropdown_item_1line,
                expenseTitles.toArray(new String[expenseTitles.size()]));
        titleView.setAdapter(expenseTitlesAdapter);
        titleView.setThreshold(1);

        createCustomSplitRows();
        updateCustomSplit();
        updatePayer();
        updateDate();

        customSplitCheckBox.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
            @Override
            public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
                customSplitTable.setVisibility(isChecked ? View.VISIBLE : View.GONE);
            }
        });

        amountView.addTextChangedListener(updateCustomSplitTextWatcher);

        payerView.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                pickPayer();
            }
        });

        dateView.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                pickDate();
            }
        });
    }

    private void createCustomSplitRows() {
        Map<Long, Double> weights = expense.getSplitWeights();
        customSplitEntries = new CustomSplitEntry[persons.size()];
        int dynamicId = 0;

        LayoutInflater inflater = getLayoutInflater();
        for (int i = 0; i < persons.size(); i++) {
            Person person = persons.get(i);
            boolean enabled = true;
            Double weight = 1.0;

            if (weights != null) {
                Double w = weights.get(person.getId());
                if (w != null)
                    weight = w;
                else
                    enabled = false;
            }

            TableRow row = (TableRow) inflater.inflate(R.layout.split_row, customSplitTable, false);
            customSplitTable.addView(row);

            final CustomSplitEntry customSplitEntry = new CustomSplitEntry();
            customSplitEntries[i] = customSplitEntry;

            customSplitEntry.enabled = (CheckBox) row.findViewById(R.id.split_enabled);
            customSplitEntry.enabled.setId(dynamicId++);
            customSplitEntry.enabled.setText(person.getName() + ":");
            customSplitEntry.enabled.setChecked(enabled);
            customSplitEntry.enabled.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
                @Override
                public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
                    customSplitEntry.weight.setEnabled(isChecked);
                    updateCustomSplit();
                }
            });

            customSplitEntry.weight = (EditText) row.findViewById(R.id.split_weight);
            customSplitEntry.weight.setId(dynamicId++);
            customSplitEntry.weight.setEnabled(enabled);
            customSplitEntry.weight.setText(currencyHelper.format(weight, false));
            customSplitEntry.weight.addTextChangedListener(updateCustomSplitTextWatcher);

            customSplitEntry.result = (TextView) row.findViewById(R.id.split_share);
        }

        customSplitCheckBox.setChecked(weights != null);
        customSplitTable.setVisibility(weights != null ? View.VISIBLE : View.GONE);
    }

    private void updateCustomSplit() {
        try {
            for (CustomSplitEntry customSplitEntry : customSplitEntries)
                customSplitEntry.result.setText("");

            double[] weights = new double[customSplitEntries.length];
            double weightSum = 0;
            for (int i = 0; i < customSplitEntries.length; i++) {
                weights[i] = 0;
                if (customSplitEntries[i].enabled.isChecked())
                    weights[i] = getWeight(i);
                weightSum += weights[i];
            }

            double amount = getAmount();
            for (int i = 0; i < customSplitEntries.length; i++) {
                if (customSplitEntries[i].enabled.isChecked() && weightSum > 0) {
                    double share = (weights[i] / weightSum * amount);
                    String formatted = currencyHelper.format(share);
                    customSplitEntries[i].result.setText(formatted);
                }
            }
        } catch (Exception ignored) {
        }
    }

    private final TextWatcher updateCustomSplitTextWatcher = new TextWatcher() {
        @Override
        public void onTextChanged(CharSequence s, int start, int before, int count) {
        }

        @Override
        public void beforeTextChanged(CharSequence s, int start, int count, int after) {
        }

        @Override
        public void afterTextChanged(Editable s) {
            updateCustomSplit();
        }
    };

    private void updateCurrency() {
        java.util.Currency c = (java.util.Currency) currencySpinner.getSelectedItem();
        for (Currency currency : calculation.getCurrencies())
            if (currency.getCurrencyCode().equals(c.getCurrencyCode()))
                expense.setCurrency(currency);
        currencyHelper = expense.getCurrency().getCurrencyHelper();
        updateCustomSplit();
    }

    private void updateDate() {
        DateFormat format = DateFormat.getDateInstance();
        dateView.setText(format.format(expense.getDate().getTime()));
    }

    private void updatePayer() {
        payerView.setText(R.string.expense_payer_prompt);
        if (expense.getPerson() != null)
            payerView.setText(expense.getPerson().getName());
    }

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        getMenuInflater().inflate(R.menu.expense_editor_options, menu);
        menu.findItem(R.id.menu_delete).setVisible(mode == Mode.EDIT_EXPENSE);
        return true;
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        switch (item.getItemId()) {
        case R.id.menu_save:
            doSave();
            return true;
        case R.id.menu_delete:
            doDelete();
            return true;
        default:
            return super.onOptionsItemSelected(item);
        }
    }

    @Override
    protected void onPause() {
        super.onPause();
        dbHelper.close();
    }

    private void pickDate() {
        final DatePickerDialog.OnDateSetListener onDateSet = new DatePickerDialog.OnDateSetListener() {
            @Override
            public void onDateSet(DatePicker view, int year, int month, int day) {
                Calendar date = Calendar.getInstance();
                date.clear();
                date.set(year, month, day);
                expense.setDate(date);
                updateDate();
            }
        };

        DialogFragment fragment = new DialogFragment() {
            @Override
            public Dialog onCreateDialog(Bundle savedInstanceState) {
                Calendar date = expense.getDate();
                int year = date.get(Calendar.YEAR);
                int month = date.get(Calendar.MONTH);
                int day = date.get(Calendar.DAY_OF_MONTH);
                return new DatePickerDialog(getActivity(), onDateSet, year, month, day);
            }
        };
        fragment.show(getSupportFragmentManager(), "datePicker");
    }

    private void pickPayer() {
        DialogFragment fragment = new DialogFragment() {
            @Override
            public Dialog onCreateDialog(Bundle savedInstanceState) {
                CharSequence[] personsArray = new CharSequence[persons.size()];
                int selected = -1;
                for (int i = 0; i < persons.size(); i++) {
                    Person person = persons.get(i);
                    personsArray[i] = person.getName();
                    if (person.equals(expense.getPerson()))
                        selected = i;
                }

                AlertDialog.Builder builder = new AlertDialog.Builder(ExpenseEditorActivity.this);
                builder.setTitle(R.string.expense_payer_prompt);
                builder.setSingleChoiceItems(personsArray, selected, new DialogInterface.OnClickListener() {
                    @Override
                    public void onClick(DialogInterface dialog, int i) {
                        Person payer = persons.get(i);
                        expense.setPerson(payer);
                        updatePayer();
                        payerView.setError(null);
                        dismiss();
                    }
                });
                return builder.create();
            }
        };
        fragment.show(getSupportFragmentManager(), "personSelector");
    }

    private String getExpenseTitle() {
        return titleView.getText().toString().trim();
    }

    private double getAmount() throws ParseException {
        String amountString = amountView.getText().toString();
        return currencyHelper.parse(amountString);
    }

    private double getWeight(int i) throws ParseException {
        String amountString = customSplitEntries[i].weight.getText().toString();
        Number amountNumber = NumberFormat.getNumberInstance().parse(amountString);
        return amountNumber.doubleValue();
    }

    private boolean validate() {
        final Resources res = getResources();
        final String errRequired = res.getString(R.string.validate_required);
        final String errNumber = res.getString(R.string.validate_number);
        final String errSplit = res.getString(R.string.validate_select_split_persons);

        boolean valid = true;

        titleView.setError(null);
        amountView.setError(null);
        payerView.setError(null);
        customSplitCheckBox.setError(null);
        for (CustomSplitEntry customSplitEntry : customSplitEntries)
            customSplitEntry.weight.setError(null);

        if (getExpenseTitle().length() == 0) {
            titleView.setError(errRequired);
            valid = false;
        }

        try {
            getAmount();
        } catch (Exception e) {
            amountView.setError(errNumber);
            valid = false;
        }

        if (expense.getPerson() == null) {
            payerView.setError(errRequired);
            valid = false;
        }

        if (customSplitCheckBox.isChecked()) {
            int numEnabled = 0;

            for (int i = 0; i < customSplitEntries.length; i++) {
                if (customSplitEntries[i].enabled.isChecked()) {
                    numEnabled++;
                    try {
                        double weight = getWeight(i);
                        if (weight < 0.01)
                            customSplitEntries[i].weight.setError(errNumber);
                    } catch (Exception e) {
                        customSplitEntries[i].weight.setError(errNumber);
                        valid = false;
                    }
                }
            }

            if (numEnabled == 0) {
                customSplitCheckBox.setError(errSplit);
                valid = false;
            }
        }

        return valid;
    }

    private void save() throws ParseException {
        expense.setTitle(getExpenseTitle());
        expense.setAmount(getAmount());

        Map<Long, Double> weights = null;
        if (customSplitCheckBox.isChecked()) {
            weights = new HashMap<>();
            for (int i = 0; i < customSplitEntries.length; i++)
                if (customSplitEntries[i].enabled.isChecked())
                    weights.put(persons.get(i).getId(), getWeight(i));
        }
        expense.setSplitWeights(weights);

        if (mode == Mode.EDIT_EXPENSE)
            expenseDataSource.update(expense);
        else
            expenseDataSource.insert(expense);

        finish();
    }

    private void doSave() {
        try {
            if (validate())
                save();
        } catch (Exception e) {
            Toast.makeText(this, "Error saving expense", Toast.LENGTH_LONG).show();
        }
    }

    private void doDelete() {
        expenseDataSource.delete(expense.getId());
        finish();
    }

}