/*
* Copyright (C) 2012-2016 The Android Money Manager Ex Project Team
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 3
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.money.manager.ex.common;
import android.app.Dialog;
import android.content.Context;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.v4.app.DialogFragment;
import android.text.TextUtils;
import android.view.LayoutInflater;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
import android.widget.TextView;
import com.afollestad.materialdialogs.DialogAction;
import com.afollestad.materialdialogs.MaterialDialog;
import com.money.manager.ex.Constants;
import com.money.manager.ex.R;
import com.money.manager.ex.common.events.AmountEnteredEvent;
import com.money.manager.ex.core.FormatUtilities;
import com.money.manager.ex.core.NumericHelper;
import com.money.manager.ex.core.bundlers.MoneyBundler;
import com.money.manager.ex.currency.CurrencyService;
import com.money.manager.ex.domainmodel.Currency;
import com.shamanland.fonticon.FontIconView;
import net.objecthunter.exp4j.ExpressionBuilder;
import org.greenrobot.eventbus.EventBus;
import icepick.Icepick;
import icepick.State;
import info.javaperformance.money.Money;
import info.javaperformance.money.MoneyFactory;
import timber.log.Timber;
public class AmountInputDialog
extends DialogFragment {
private static final String KEY_REQUEST_ID = "AmountInputDialog:Id";
private static final String KEY_AMOUNT = "AmountInputDialog:Amount";
private static final String KEY_CURRENCY_ID = "AmountInputDialog:CurrencyId";
private static final String KEY_EXPRESSION = "AmountInputDialog:Expression";
private static final String ARG_ROUNDING = "AmountInputDialog:Rounding";
public static AmountInputDialog getInstance(int requestId, Money amount) {
String requestIdString = Integer.toString(requestId);
return getInstance(requestIdString, amount, null, false);
}
public static AmountInputDialog getInstance(String requestId, Money amount) {
return getInstance(requestId, amount, null, false);
}
public static AmountInputDialog getInstance(int requestId, Money amount, int currencyId) {
String requestIdString = Integer.toString(requestId);
return getInstance(requestIdString, amount, currencyId, true);
}
public static AmountInputDialog getInstance(String requestId, Money amount, int currencyId) {
return getInstance(requestId, amount, currencyId, true);
}
public static AmountInputDialog getInstance(int requestId, Money amount, Integer currencyId,
boolean roundToCurrencyDecimals) {
String requestIdString = Integer.toString(requestId);
return getInstance(requestIdString, amount, currencyId, roundToCurrencyDecimals);
}
public static AmountInputDialog getInstance(String requestId, Money amount, Integer currencyId,
boolean roundToCurrencyDecimals) {
Bundle args = new Bundle();
args.putString(KEY_REQUEST_ID, requestId);
String amountString = amount == null ? "0" : amount.toString();
args.putString(KEY_AMOUNT, amountString);
if (currencyId == null) currencyId = Constants.NOT_SET;
args.putInt(KEY_CURRENCY_ID, currencyId);
args.putBoolean(ARG_ROUNDING, roundToCurrencyDecimals);
AmountInputDialog dialog = new AmountInputDialog();
dialog.setArguments(args);
return dialog;
}
/**
* By default, round the number to the currency Scale. Set in the factory method.
*/
public boolean roundToCurrencyDecimals;
private int[] idButtonKeyNum = {
R.id.buttonKeyNum0, R.id.buttonKeyNum1, R.id.buttonKeyNum2, R.id.buttonKeyNum3,
R.id.buttonKeyNum4, R.id.buttonKeyNum5, R.id.buttonKeyNum6, R.id.buttonKeyNum7,
R.id.buttonKeyNum8, R.id.buttonKeyNum9,
R.id.buttonKeyNumDecimal,
};
private int[] idOperatorKeys = {
R.id.buttonKeyAdd, R.id.buttonKeyDiv,
R.id.buttonKeyLess, R.id.buttonKeyMultiplication,
R.id.buttonKeyLeftParenthesis, R.id.buttonKeyRightParenthesis
};
private String mRequestId;
@State(MoneyBundler.class) Money mAmount;
private Integer mCurrencyId;
private Integer mDefaultColor;
private TextView txtMain, txtTop;
private CurrencyService mCurrencyService;
/**
* used to restore expression from saved instance state.
*/
private String mExpression;
/**
* Indicates that the user has already started typing. We should not replace the existing number
* with the typed value but append the typed value to the existing number.
*/
private boolean mStartedTyping = false;
private FormatUtilities formatUtilities;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mCurrencyService = new CurrencyService(getContext());
this.formatUtilities = new FormatUtilities(getActivity());
// get arguments
restoreArguments();
if (savedInstanceState != null) {
restoreSavedInstanceState(savedInstanceState);
} else {
initializeNewDialog();
}
}
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
LayoutInflater inflater = ((LayoutInflater) getActivity().getSystemService(Context.LAYOUT_INFLATER_SERVICE));
View view = inflater.inflate(R.layout.input_amount_dialog, null);
// set the decimal separator according to the locale
setDecimalSeparator(view);
// Numbers and Operators.
OnClickListener numberClickListener = new OnClickListener() {
@Override
public void onClick(View v) {
// Reset prior value/text (in some cases only).
String existingValue = txtMain.getText().toString();
if (!mStartedTyping) {
existingValue = "";
mStartedTyping = true;
}
txtMain.setText(existingValue.concat(((Button) v).getText().toString()));
evalExpression();
}
};
for (int id : idButtonKeyNum) {
Button button = (Button) view.findViewById(id);
button.setOnClickListener(numberClickListener);
}
OnClickListener operatorClickListener = new OnClickListener() {
@Override
public void onClick(View v) {
String existingValue = txtMain.getText().toString();
mStartedTyping = true;
txtMain.setText(existingValue.concat(((Button) v).getText().toString()));
evalExpression();
}
};
for (int id : idOperatorKeys) {
Button button = (Button) view.findViewById(id);
button.setOnClickListener(operatorClickListener);
}
// Clear button. 'C'
Button clearButton = (Button) view.findViewById(R.id.buttonKeyClear);
clearButton.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View view) {
mStartedTyping = true;
txtMain.setText("");
evalExpression();
}
});
// Equals button '='
Button buttonKeyEquals = (Button) view.findViewById(R.id.buttonKeyEqual);
buttonKeyEquals.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
// this is called only to reset the warning colour in the top box, if any.
evalExpression();
showAmountInEntryField();
}
});
// Delete button '<='
FontIconView deleteButton = (FontIconView) view.findViewById(R.id.deleteButton);
if (deleteButton != null) {
deleteButton.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
mStartedTyping = true;
String currentNumber = txtMain.getText().toString();
currentNumber = deleteLastCharacterFrom(currentNumber);
txtMain.setText(currentNumber);
evalExpression();
}
});
}
// Amounts
txtTop = (TextView) view.findViewById(R.id.textViewTop);
mDefaultColor = txtTop.getCurrentTextColor();
txtMain = (TextView) view.findViewById(R.id.textViewMain);
if (!TextUtils.isEmpty(mExpression)) {
txtMain.setText(mExpression);
} else {
showAmountInEntryField();
}
// evaluate the expression initially, in case there is an existing amount passed to the binaryDialog.
evalExpression();
// Dialog
MaterialDialog.Builder builder = new MaterialDialog.Builder(getActivity())
.customView(view, false)
.cancelable(false)
.onPositive(new MaterialDialog.SingleButtonCallback() {
@Override
public void onClick(@NonNull MaterialDialog dialog, @NonNull DialogAction which) {
if (!evalExpression()) return;
EventBus.getDefault().post(new AmountEnteredEvent(mRequestId, getAmount()));
dialog.dismiss();
}
})
.onNegative(new MaterialDialog.SingleButtonCallback() {
@Override
public void onClick(@NonNull MaterialDialog dialog, @NonNull DialogAction which) {
dialog.dismiss();
}
})
.autoDismiss(false)
.negativeText(android.R.string.cancel)
.positiveText(android.R.string.ok);
return builder.show();
}
@Override
public void onResume() {
super.onResume();
displayFormattedAmount();
}
@Override
public void onSaveInstanceState(Bundle savedInstanceState) {
super.onSaveInstanceState(savedInstanceState);
if (mCurrencyId != null) savedInstanceState.putInt(KEY_CURRENCY_ID, mCurrencyId);
savedInstanceState.putString(KEY_REQUEST_ID, mRequestId);
mExpression = txtMain.getText().toString();
savedInstanceState.putString(KEY_EXPRESSION, mExpression);
Icepick.saveInstanceState(this, savedInstanceState);
}
// methods
/**
* Displays the expression result in the top text box. This is a formatted number in the
* given currency.
*/
public void displayFormattedAmount() {
String result = getFormattedAmount();
txtTop.setText(result);
}
/**
* Evaluate the entered expression and recalculate the resulting amount.
* @return Boolean indicating whether the operation was successful or not.
*/
public boolean evalExpression() {
String exp = txtMain.getText().toString();
NumericHelper helper = new NumericHelper(getContext());
exp = helper.cleanUpNumberString(exp);
if (exp.length() > 0) {
try {
Double result = new ExpressionBuilder(exp).build().evaluate();
int precision = getPrecision();
mAmount = MoneyFactory.fromString(Double.toString(result))
.truncate(precision);
} catch (IllegalArgumentException ex) {
// Just display the last valid value.
displayFormattedAmount();
// Use the warning colour.
txtTop.setTextColor(getResources().getColor(R.color.material_amber_800));
return false;
} catch (Exception e) {
Timber.e(e, "evaluating expression");
}
} else {
mAmount = MoneyFactory.fromString("0");
}
displayFormattedAmount();
txtTop.setTextColor(mDefaultColor);
return true;
}
/**
* Get amount formatted in the formatting currency.
* @return String Amount formatted in the given currency.
*/
public String getFormattedAmount() {
String result = null;
FormatUtilities format = new FormatUtilities(getActivity());
// No currency. Use locale preferences.
if (mCurrencyId == null) {
result = format.formatWithLocale(mAmount);
}
// Use currency preferences but ignore the decimals.
if (!getRoundToCurrencyDecimals()) {
// ignore the currency preferences but show the symbol.
result = format.formatNumberIgnoreDecimalCount(mAmount, mCurrencyId);
}
// default format, use currency preferences.
if (result == null) {
result = mCurrencyService.getCurrencyFormatted(mCurrencyId, mAmount);
}
return result;
}
// private
private int getPrecision() {
// if using a currency and currency precision is required, use that.
if (!this.roundToCurrencyDecimals || this.mCurrencyId == null) return Constants.DEFAULT_PRECISION;
Currency currency = this.mCurrencyService.getCurrency(mCurrencyId);
if (currency == null) return Constants.DEFAULT_PRECISION;
// get precision from the currency
NumericHelper helper = new NumericHelper(getActivity());
return helper.getNumberOfDecimals(currency.getScale());
}
private String deleteLastCharacterFrom(String number) {
// check length
if (number.length() <= 0) return number;
// first cut-off the last digit
number = number.substring(0, number.length() - 1);
// Should we check if the next character is the decimal separator. (?)
// Handle deleting the last number - set the remaining value to 0.
if (TextUtils.isEmpty(number)) {
number = "0";
}
return number;
}
private void restoreSavedInstanceState(Bundle savedInstanceState) {
if (savedInstanceState.containsKey(KEY_REQUEST_ID)) {
mRequestId = savedInstanceState.getString(KEY_REQUEST_ID);
}
// if (savedInstanceState.containsKey(KEY_AMOUNT)) {
// mAmount = MoneyFactory.fromString(savedInstanceState.getString(KEY_AMOUNT));
// }
if (savedInstanceState.containsKey(KEY_CURRENCY_ID)) {
mCurrencyId = savedInstanceState.getInt(KEY_CURRENCY_ID);
}
if (savedInstanceState.containsKey(KEY_EXPRESSION)) {
mExpression = savedInstanceState.getString(KEY_EXPRESSION);
}
Icepick.restoreInstanceState(this, savedInstanceState);
}
/**
* Set the decimal separator to the base currency's separator.
* @param view current view
*/
private void setDecimalSeparator(View view) {
Button separatorButton = (Button) view.findViewById(R.id.buttonKeyNumDecimal);
String separator = this.formatUtilities.getDecimalSeparatorForAppLocale();
separatorButton.setText(separator);
}
private boolean isCurrencySet() {
return this.mCurrencyId != null && this.mCurrencyId != Constants.NOT_SET;
}
private Money getAmount() {
Money result;
// to round or not? Handle case when no base currency set.
if (this.roundToCurrencyDecimals && isCurrencySet()) {
NumericHelper numericHelper = new NumericHelper(getContext());
Currency currency = mCurrencyService.getCurrency(mCurrencyId);
result = numericHelper.truncateToCurrency(mAmount, currency);
} else {
result = mAmount;
}
return result;
}
private String getFormattedAmountForEditing(Money amount) {
if (amount == null) return "0";
String result;
Currency displayCurrency = mCurrencyService.getCurrency(mCurrencyId);
if (displayCurrency != null) {
if(getRoundToCurrencyDecimals()) {
// use decimals from the display currency.
// but decimal and group separators from the base currency.
result = formatUtilities.format(amount, displayCurrency.getScale(),
formatUtilities.getDecimalSeparatorForAppLocale(),
formatUtilities.getGroupingSeparatorForAppLocale());
} else {
// Use default precision and no currency markup.
result = formatUtilities.formatNumber(amount, Constants.DEFAULT_PRECISION,
displayCurrency.getDecimalSeparator(), displayCurrency.getGroupSeparator(),
null, null);
}
} else {
result = formatUtilities.formatWithLocale(amount);
}
return result;
}
private boolean getRoundToCurrencyDecimals() {
return getArguments().getBoolean(ARG_ROUNDING);
}
private void initializeNewDialog() {
// not in restored state. new binaryDialog
// Display the existing amount, if any has been passed into the binaryDialog.
NumericHelper numericHelper = new NumericHelper(getContext());
Currency currency = mCurrencyService.getCurrency(mCurrencyId);
Money amount = MoneyFactory.fromString(getArguments().getString(KEY_AMOUNT));
if (currency != null && this.roundToCurrencyDecimals) {
mAmount = numericHelper.truncateToCurrency(amount, currency);
} else {
// no currency and no base currency set.
mAmount = amount;
}
}
private void restoreArguments() {
Bundle args = getArguments();
this.mRequestId = args.getString(KEY_REQUEST_ID);
this.mCurrencyId = args.getInt(KEY_CURRENCY_ID);
this.roundToCurrencyDecimals = args.getBoolean(ARG_ROUNDING);
}
private void showAmountInEntryField() {
// Get the calculated amount in default locale and display in the main box.
String amount = getFormattedAmountForEditing(mAmount);
txtMain.setText(amount);
}
}