package com.code44.finance.ui.transactions; import android.app.Activity; import android.content.Context; import android.content.Intent; import android.database.Cursor; import android.os.Bundle; import android.os.Parcelable; import android.support.v4.content.CursorLoader; import android.text.Editable; import android.text.SpannableStringBuilder; import android.text.Spanned; import android.text.TextWatcher; import android.view.View; import android.widget.Button; import android.widget.CheckBox; import android.widget.CompoundButton; import android.widget.EditText; import android.widget.ImageButton; import android.widget.ImageView; import com.code44.finance.R; import com.code44.finance.api.currencies.CurrenciesApi; import com.code44.finance.api.currencies.ExchangeRateRequest; import com.code44.finance.common.model.TransactionState; import com.code44.finance.common.model.TransactionType; import com.code44.finance.common.utils.StringUtils; import com.code44.finance.data.DataStore; import com.code44.finance.data.db.Tables; import com.code44.finance.data.model.Account; import com.code44.finance.data.model.Category; import com.code44.finance.data.model.Currency; import com.code44.finance.data.model.Tag; import com.code44.finance.data.model.Transaction; import com.code44.finance.data.providers.TransactionsProvider; import com.code44.finance.qualifiers.Main; import com.code44.finance.ui.CalculatorActivity; import com.code44.finance.ui.ModelListActivityOld; import com.code44.finance.ui.accounts.AccountsActivity; import com.code44.finance.ui.categories.CategoriesActivity; import com.code44.finance.ui.common.ModelEditActivity; import com.code44.finance.ui.dialogs.DatePickerDialog; import com.code44.finance.ui.dialogs.TimePickerDialog; import com.code44.finance.ui.tags.TagsActivity; import com.code44.finance.utils.FieldValidationUtils; import com.code44.finance.utils.MoneyFormatter; import com.code44.finance.utils.TextBackgroundSpan; import com.code44.finance.utils.analytics.Analytics; import com.code44.finance.utils.transaction.TransactionAutoComplete; import com.squareup.otto.Subscribe; import net.danlew.android.joda.DateUtils; import org.joda.time.DateTime; import java.text.DecimalFormat; import java.text.NumberFormat; import java.util.ArrayList; import java.util.List; import java.util.Locale; import javax.inject.Inject; public class TransactionEditActivity extends ModelEditActivity<Transaction> implements View.OnClickListener, CompoundButton.OnCheckedChangeListener, TransactionAutoComplete.TransactionAutoCompleteListener, View.OnLongClickListener { private static final int REQUEST_AMOUNT = 1; private static final int REQUEST_ACCOUNT_FROM = 2; private static final int REQUEST_ACCOUNT_TO = 3; private static final int REQUEST_CATEGORY = 4; private static final int REQUEST_TAGS = 5; private static final int REQUEST_DATE = 6; private static final int REQUEST_TIME = 7; private static final int REQUEST_EXCHANGE_RATE = 8; private static final int REQUEST_AMOUNT_TO = 9; @Inject CurrenciesApi currenciesApi; @Inject @Main Currency mainCurrency; @Inject TransactionAutoComplete transactionAutoComplete; private Button dateButton; private Button timeButton; private ImageButton transactionTypeImageButton; private Button amountButton; private Button exchangeRateButton; private Button amountToButton; private Button accountFromButton; private Button accountToButton; private ImageView colorImageView; private Button categoryButton; private Button tagsButton; private EditText noteEditText; private CheckBox confirmedCheckBox; private CheckBox includeInReportsCheckBox; private Button saveButton; public static void start(Context context, String transactionServerId) { startActivity(context, makeIntent(context, TransactionEditActivity.class, transactionServerId)); } @Override protected int getLayoutId() { return R.layout.activity_transaction_edit; } @Override protected void onViewCreated(Bundle savedInstanceState) { super.onViewCreated(savedInstanceState); // Get views transactionTypeImageButton = (ImageButton) findViewById(R.id.transactionTypeImageButton); amountButton = (Button) findViewById(R.id.amountButton); exchangeRateButton = (Button) findViewById(R.id.exchangeRateButton); amountToButton = (Button) findViewById(R.id.amountToButton); accountFromButton = (Button) findViewById(R.id.accountFromButton); accountToButton = (Button) findViewById(R.id.accountToButton); colorImageView = (ImageView) findViewById(R.id.colorImageView); categoryButton = (Button) findViewById(R.id.categoryButton); tagsButton = (Button) findViewById(R.id.tagsButton); dateButton = (Button) findViewById(R.id.dateButton); timeButton = (Button) findViewById(R.id.timeButton); noteEditText = (EditText) findViewById(R.id.noteEditText); confirmedCheckBox = (CheckBox) findViewById(R.id.confirmedCheckBox); includeInReportsCheckBox = (CheckBox) findViewById(R.id.includeInReportsCheckBox); saveButton = (Button) findViewById(R.id.saveButton); // Setup transactionTypeImageButton.setOnClickListener(this); amountButton.setOnClickListener(this); amountButton.setOnLongClickListener(this); exchangeRateButton.setOnClickListener(this); exchangeRateButton.setOnLongClickListener(this); amountToButton.setOnClickListener(this); amountButton.setOnLongClickListener(this); accountFromButton.setOnClickListener(this); accountFromButton.setOnLongClickListener(this); accountToButton.setOnClickListener(this); accountToButton.setOnLongClickListener(this); categoryButton.setOnClickListener(this); categoryButton.setOnLongClickListener(this); tagsButton.setOnClickListener(this); tagsButton.setOnLongClickListener(this); dateButton.setOnClickListener(this); dateButton.setOnLongClickListener(this); timeButton.setOnClickListener(this); timeButton.setOnLongClickListener(this); confirmedCheckBox.setOnCheckedChangeListener(this); includeInReportsCheckBox.setOnCheckedChangeListener(this); noteEditText.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) { if (model != null) { model.setNote(noteEditText.getText().toString()); } } @Override public void afterTextChanged(Editable s) { } }); final boolean isAutoAmountRequested = savedInstanceState != null; if ((StringUtils.isEmpty(modelId) || modelId.equals("0")) && !isAutoAmountRequested) { CalculatorActivity.start(this, REQUEST_AMOUNT, 0); } } @Override public void onResume() { super.onResume(); getEventBus().register(this); transactionAutoComplete.setListener(this); } @Override public void onPause() { super.onPause(); getEventBus().unregister(this); transactionAutoComplete.setListener(null); } @Override public void onActivityResult(int requestCode, int resultCode, Intent data) { if (resultCode == Activity.RESULT_OK) { switch (requestCode) { case REQUEST_AMOUNT: model.setAmount(data.getLongExtra(CalculatorActivity.RESULT_EXTRA_RESULT, 0)); onModelLoaded(model); transactionAutoComplete.setAmount(model.getAmount()); return; case REQUEST_ACCOUNT_FROM: model.setAccountFrom(data.<Account>getParcelableExtra(ModelListActivityOld.RESULT_EXTRA_MODEL)); onModelLoaded(model); transactionAutoComplete.setAccountFrom(model.getAccountFrom()); refreshExchangeRate(); return; case REQUEST_ACCOUNT_TO: model.setAccountTo(data.<Account>getParcelableExtra(ModelListActivityOld.RESULT_EXTRA_MODEL)); onModelLoaded(model); transactionAutoComplete.setAccountTo(model.getAccountTo()); refreshExchangeRate(); return; case REQUEST_CATEGORY: model.setCategory(data.<Category>getParcelableExtra(ModelListActivityOld.RESULT_EXTRA_MODEL)); onModelLoaded(model); transactionAutoComplete.setCategory(model.getCategory()); return; case REQUEST_TAGS: final Parcelable[] parcelables = data.getParcelableArrayExtra(ModelListActivityOld.RESULT_EXTRA_MODELS); final List<Tag> tags = new ArrayList<>(); for (Parcelable parcelable : parcelables) { tags.add((Tag) parcelable); } model.setTags(tags); onModelLoaded(model); transactionAutoComplete.setTags(tags); return; case REQUEST_EXCHANGE_RATE: model.setExchangeRate(data.getDoubleExtra(CalculatorActivity.RESULT_EXTRA_RAW_RESULT, 1.0)); if (Double.compare(model.getExchangeRate(), 0) <= 0) { model.setExchangeRate(1.0); } onModelLoaded(model); return; case REQUEST_AMOUNT_TO: final long amountTo = data.getLongExtra(CalculatorActivity.RESULT_EXTRA_RESULT, 0); if (Double.compare(model.getExchangeRate(), 0) == 0) { model.setExchangeRate(1.0); } final long amount = Math.round(amountTo / model.getExchangeRate()); model.setAmount(amount); onModelLoaded(model); transactionAutoComplete.setAmount(model.getAmount()); return; } } super.onActivityResult(requestCode, resultCode, data); } @Override protected boolean onSave(Transaction model) { model.setTransactionState(model.getTransactionState() == TransactionState.Confirmed && canBeConfirmed(model, false) ? TransactionState.Confirmed : TransactionState.Pending); DataStore.insert().values(model.asValues()).into(this, TransactionsProvider.uriTransactions()); return true; } @Override protected void ensureModelUpdated(Transaction model) { model.setNote(noteEditText.getText().toString()); } @Override protected CursorLoader getModelCursorLoader(String modelId) { return Tables.Transactions.getQuery().asCursorLoader(this, TransactionsProvider.uriTransaction(modelId)); } @Override protected Transaction getModelFrom(Cursor cursor) { final Transaction transaction = Transaction.from(cursor); if (!transaction.hasId()) { transactionAutoComplete.setListener(this); transactionAutoComplete.setTransaction(transaction); } return transaction; } @Override protected void onModelLoaded(Transaction transaction) { switch (transaction.getTransactionType()) { case Expense: accountFromButton.setVisibility(View.VISIBLE); accountToButton.setVisibility(View.GONE); colorImageView.setVisibility(View.VISIBLE); categoryButton.setVisibility(View.VISIBLE); transactionTypeImageButton.setImageResource(R.drawable.ic_category_type_expense); exchangeRateButton.setVisibility(View.GONE); amountToButton.setVisibility(View.GONE); break; case Income: accountFromButton.setVisibility(View.GONE); accountToButton.setVisibility(View.VISIBLE); colorImageView.setVisibility(View.VISIBLE); categoryButton.setVisibility(View.VISIBLE); transactionTypeImageButton.setImageResource(R.drawable.ic_category_type_income); exchangeRateButton.setVisibility(View.GONE); amountToButton.setVisibility(View.GONE); break; case Transfer: accountFromButton.setVisibility(View.VISIBLE); accountToButton.setVisibility(View.VISIBLE); colorImageView.setVisibility(View.GONE); categoryButton.setVisibility(View.GONE); transactionTypeImageButton.setImageResource(R.drawable.ic_category_type_transfer); final boolean bothAccountsSet = transaction.getAccountFrom() != null && transaction.getAccountTo() != null; final boolean differentCurrencies = bothAccountsSet && !transaction.getAccountFrom().getCurrency().getId().equals(transaction.getAccountTo().getCurrency().getId()); if (bothAccountsSet && differentCurrencies) { exchangeRateButton.setVisibility(View.VISIBLE); amountToButton.setVisibility(View.VISIBLE); NumberFormat format = DecimalFormat.getInstance(Locale.ENGLISH); format.setGroupingUsed(false); exchangeRateButton.setText(format.format(model.getExchangeRate())); amountToButton.setText(MoneyFormatter.format(transaction.getAccountTo().getCurrency(), Math.round(transaction.getAmount() * transaction.getExchangeRate()))); } else { exchangeRateButton.setVisibility(View.GONE); amountToButton.setVisibility(View.GONE); } break; } final DateTime dateTime = new DateTime(transaction.getDate()); dateButton.setText(DateUtils.formatDateTime(this, dateTime, DateUtils.FORMAT_SHOW_DATE)); timeButton.setText(DateUtils.formatDateTime(this, dateTime, DateUtils.FORMAT_SHOW_TIME)); amountButton.setText(MoneyFormatter.format(getAmountCurrency(transaction), transaction.getAmount())); accountFromButton.setText(transaction.getAccountFrom() == null ? null : transaction.getAccountFrom().getTitle()); accountToButton.setText(transaction.getAccountTo() == null ? null : transaction.getAccountTo().getTitle()); colorImageView.setColorFilter(getCategoryColor(transaction)); categoryButton.setText(transaction.getCategory() == null ? null : transaction.getCategory().getTitle()); noteEditText.setText(transaction.getNote()); confirmedCheckBox.setChecked(transaction.getTransactionState() == TransactionState.Confirmed && canBeConfirmed(transaction, false)); includeInReportsCheckBox.setChecked(transaction.includeInReports()); saveButton.setText(confirmedCheckBox.isChecked() ? R.string.save : R.string.pending); final SpannableStringBuilder subtitle = new SpannableStringBuilder(); for (Tag tag : transaction.getTags()) { subtitle.append(tag.getTitle()); subtitle.setSpan(new TextBackgroundSpan(getResources().getColor(R.color.bg_secondary), getResources().getDimension(R.dimen.tag_radius)), subtitle.length() - tag.getTitle().length(), subtitle.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); subtitle.append(" "); } tagsButton.setText(subtitle); } @Override public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { switch (buttonView.getId()) { case R.id.confirmedCheckBox: if (canBeConfirmed(model, true)) { model.setTransactionState(isChecked ? TransactionState.Confirmed : TransactionState.Pending); } onModelLoaded(model); break; case R.id.includeInReportsCheckBox: model.setIncludeInReports(isChecked); break; } } @Override public void onClick(View v) { switch (v.getId()) { case R.id.transactionTypeImageButton: toggleTransactionType(); break; case R.id.amountButton: CalculatorActivity.start(this, REQUEST_AMOUNT, model.getAmount()); break; case R.id.exchangeRateButton: CalculatorActivity.start(this, REQUEST_EXCHANGE_RATE, model.getExchangeRate()); break; case R.id.amountToButton: CalculatorActivity.start(this, REQUEST_AMOUNT_TO, Math.round(model.getAmount() * model.getExchangeRate())); break; case R.id.accountFromButton: AccountsActivity.startSelect(this, REQUEST_ACCOUNT_FROM); break; case R.id.accountToButton: AccountsActivity.startSelect(this, REQUEST_ACCOUNT_TO); break; case R.id.categoryButton: CategoriesActivity.startSelect(this, REQUEST_CATEGORY, model.getTransactionType()); break; case R.id.tagsButton: TagsActivity.startMultiSelect(this, REQUEST_TAGS, model.getTags()); break; case R.id.dateButton: DatePickerDialog.show(getSupportFragmentManager(), REQUEST_DATE, model.getDate()); break; case R.id.timeButton: TimePickerDialog.show(getSupportFragmentManager(), REQUEST_TIME, model.getDate()); break; } } @Override public boolean onLongClick(View v) { switch (v.getId()) { case R.id.amountButton: model.setAmount(0); onModelLoaded(model); return true; case R.id.exchangeRateButton: model.setExchangeRate(1.0); onModelLoaded(model); return true; case R.id.amountToButton: model.setAmount(0); onModelLoaded(model); return true; case R.id.accountFromButton: model.setAccountFrom(null); onModelLoaded(model); return true; case R.id.accountToButton: model.setAccountTo(null); onModelLoaded(model); return true; case R.id.categoryButton: model.setCategory(null); onModelLoaded(model); return true; case R.id.tagsButton: model.setTags(null); onModelLoaded(model); return true; case R.id.dateButton: case R.id.timeButton: model.setDate(System.currentTimeMillis()); onModelLoaded(model); return true; } return false; } @Override public void onTransactionAutoCompleteAmounts(List<Long> amounts) { } @Override public void onTransactionAutoCompleteAccountsFrom(List<Account> accounts) { final Account newAccount = accounts.get(0); if (!newAccount.equals(model.getAccountFrom())) { model.setAccountFrom(accounts.get(0)); onModelLoaded(model); refreshExchangeRate(); } } @Override public void onTransactionAutoCompleteAccountsTo(List<Account> accounts) { final Account newAccount = accounts.get(0); if (!newAccount.equals(model.getAccountTo())) { model.setAccountTo(accounts.get(0)); onModelLoaded(model); refreshExchangeRate(); } } @Override public void onTransactionAutoCompleteCategories(List<Category> categories) { model.setCategory(categories.get(0)); onModelLoaded(model); } @Override public void onTransactionAutoCompleteTags(List<Tag> tags) { } @Override protected Analytics.Screen getScreen() { return Analytics.Screen.TransactionEdit; } @Subscribe public void onDateSet(DatePickerDialog.DateSelected dateSelected) { final DateTime date = new DateTime(model.getDate()) .withYear(dateSelected.getYear()) .withMonthOfYear(dateSelected.getMonthOfYear()) .withDayOfMonth(dateSelected.getDayOfMonth()); model.setDate(date.getMillis()); onModelLoaded(model); transactionAutoComplete.setDate(model.getDate()); } @Subscribe public void onTimeSet(TimePickerDialog.TimeSelected timeSelected) { final DateTime date = new DateTime(model.getDate()) .withHourOfDay(timeSelected.getHourOfDay()) .withMinuteOfHour(timeSelected.getMinute()); model.setDate(date.getMillis()); onModelLoaded(model); transactionAutoComplete.setDate(model.getDate()); } @Subscribe public void onExchangeRateUpdated(ExchangeRateRequest request) { if (!request.isError() && model.getAccountFrom() != null && model.getAccountTo() != null && model.getAccountFrom().getCurrency().getCode().equals(request.getFromCode()) && model.getAccountTo().getCurrency().getCode().equals(request.getToCode())) { setExchangeRate(request.getCurrency().getExchangeRate()); } } private void toggleTransactionType() { switch (model.getTransactionType()) { case Expense: model.setTransactionType(TransactionType.Income); model.setAccountFrom(null); break; case Income: model.setTransactionType(TransactionType.Transfer); model.setAccountTo(null); break; case Transfer: model.setTransactionType(TransactionType.Expense); model.setCategory(null); break; } model.setCategory(null); onModelLoaded(model); transactionAutoComplete.setTransactionType(model.getTransactionType()); } private Currency getAmountCurrency(Transaction transaction) { Currency transactionCurrency; switch (transaction.getTransactionType()) { case Expense: transactionCurrency = transaction.getAccountFrom() == null ? null : transaction.getAccountFrom().getCurrency(); break; case Income: transactionCurrency = transaction.getAccountTo() == null ? null : transaction.getAccountTo().getCurrency(); break; case Transfer: transactionCurrency = transaction.getAccountFrom() == null ? null : transaction.getAccountFrom().getCurrency(); break; default: throw new IllegalStateException("Category type " + transaction.getTransactionType() + " is not supported."); } if (transactionCurrency == null || !transactionCurrency.hasId()) { // When account is not selected yet, we use main currency. transactionCurrency = mainCurrency; } return transactionCurrency; } private int getCategoryColor(Transaction transaction) { if (transaction.getCategory() == null) { switch (transaction.getTransactionType()) { case Expense: return getResources().getColor(R.color.text_negative); case Income: return getResources().getColor(R.color.text_positive); case Transfer: return getResources().getColor(R.color.text_neutral); default: throw new IllegalArgumentException("Transaction type " + transaction.getTransactionType() + " is not supported."); } } else { return transaction.getCategory().getColor(); } } private void refreshExchangeRate() { switch (model.getTransactionType()) { case Expense: model.setExchangeRate(1); break; case Income: model.setExchangeRate(1); break; case Transfer: if (model.getAccountFrom() != null && model.getAccountTo() != null) { final Currency currencyFrom = model.getAccountFrom().getCurrency(); final Currency currencyTo = model.getAccountTo().getCurrency(); if (currencyFrom.isDefault() || currencyTo.isDefault()) { if (currencyFrom.isDefault()) { setExchangeRate(1.0 / currencyTo.getExchangeRate()); } else { setExchangeRate(currencyFrom.getExchangeRate()); } } else { currenciesApi.getExchangeRate(model.getAccountFrom().getCurrency().getCode(), model.getAccountTo().getCurrency().getCode()); } } break; } } private void setExchangeRate(double exchangeRate) { model.setExchangeRate(exchangeRate); if (Double.compare(model.getExchangeRate(), 0) <= 0) { model.setExchangeRate(1.0); } onModelLoaded(model); } private boolean canBeConfirmed(Transaction model, boolean showErrors) { boolean canBeConfirmed = validateAmount(showErrors); switch (model.getTransactionType()) { case Expense: canBeConfirmed = validateAccountFrom(showErrors) && canBeConfirmed; break; case Income: canBeConfirmed = validateAccountTo(showErrors) && canBeConfirmed; break; case Transfer: canBeConfirmed = validateAccountFrom(showErrors) && canBeConfirmed; canBeConfirmed = validateAccountTo(showErrors) && canBeConfirmed; canBeConfirmed = validateAccounts(showErrors) && canBeConfirmed; break; } return canBeConfirmed; } private boolean validateAmount(boolean showError) { if (model.getAmount() <= 0) { if (showError) { FieldValidationUtils.onError(amountButton); } return false; } return true; } private boolean validateAccountFrom(boolean showError) { if (model.getAccountFrom() == null || !model.getAccountFrom().hasId()) { if (showError) { FieldValidationUtils.onError(accountFromButton); } return false; } return true; } private boolean validateAccountTo(boolean showError) { if (model.getAccountTo() == null || !model.getAccountTo().hasId()) { if (showError) { FieldValidationUtils.onError(accountToButton); } return false; } return true; } private boolean validateAccounts(boolean showError) { if (model.getAccountTo() != null && model.getAccountFrom() != null && model.getAccountTo().hasId() && model.getAccountTo().getId().equals(model.getAccountFrom().getId())) { if (showError) { FieldValidationUtils.onError(accountFromButton); FieldValidationUtils.onError(accountToButton); } return false; } return true; } }