/*
* 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();
}
}