/*
* 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.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.text.TextUtils;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.widget.Button;
import android.widget.ImageButton;
import android.widget.TextView;
import com.mikepenz.google_material_typeface_library.GoogleMaterial;
import com.money.manager.ex.Constants;
import com.money.manager.ex.MoneyManagerApplication;
import com.money.manager.ex.R;
import com.money.manager.ex.core.FormatUtilities;
import com.money.manager.ex.core.MenuHelper;
import com.money.manager.ex.core.NumericHelper;
import com.money.manager.ex.core.UIHelper;
import com.money.manager.ex.core.bundlers.MoneyBundler;
import com.money.manager.ex.currency.CurrencyService;
import com.money.manager.ex.domainmodel.Currency;
import net.objecthunter.exp4j.ExpressionBuilder;
import javax.inject.Inject;
import butterknife.BindView;
import butterknife.ButterKnife;
import butterknife.OnClick;
import dagger.Lazy;
import icepick.State;
import info.javaperformance.money.Money;
import info.javaperformance.money.MoneyFactory;
import timber.log.Timber;
/**
* Activity for the full-screen numeric input.
* Additional functionality includes currency conversion.
*/
public class CalculatorActivity
extends MmxBaseFragmentActivity {
public static String EXTRA_CURRENCY_ID = "CurrencyId";
public static String EXTRA_AMOUNT = "Amount";
public static String EXTRA_ROUND_TO_CURRENCY = "RountToCurrencyDecimals";
public static String RESULT_AMOUNT = "AmountEntered";
/**
* By default, round the number to the currency Scale. Set in the factory method.
*/
@State boolean roundToCurrencyDecimals;
// @State String mRequestId;
@State(MoneyBundler.class) Money mAmount;
@State Integer mCurrencyId;
/**
* used to restore expression from saved instance state.
*/
@State String mExpression;
// Views
@BindView(R.id.deleteButton) ImageButton deleteButton;
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
};
@Inject CurrencyService mCurrencyService;
@Inject Lazy<FormatUtilities> formatUtilitiesLazy;
private Integer mDefaultColor;
private TextView txtMain, txtTop;
/**
* 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;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_calculator);
MoneyManagerApplication.getApp().iocComponent.inject(this);
ButterKnife.bind(this);
setDisplayHomeAsUpEnabled(true);
getSupportActionBar().setTitle(R.string.enter_amount);
if (savedInstanceState == null) {
extractArguments();
}
initializeControls();
}
@Override
public void onResume() {
super.onResume();
displayFormattedAmount();
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
new MenuHelper(this, menu).addSaveToolbarIcon();
return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
int id = item.getItemId();
switch (id) {
case MenuHelper.save:
returnResult();
return true;
}
return super.onOptionsItemSelected(item);
}
/**
* 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(this);
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 = formatUtilitiesLazy.get();
// No currency. Use locale preferences.
if (mCurrencyId == null) {
result = format.formatWithLocale(mAmount);
}
// Use currency preferences but ignore the decimals.
if (!roundToCurrencyDecimals) {
// 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;
}
/*
Events
*/
/**
* Delete button '<='
*/
@OnClick(R.id.deleteButton)
public void onDeleteClicked() {
mStartedTyping = true;
String currentNumber = txtMain.getText().toString();
currentNumber = deleteLastCharacterFrom(currentNumber);
txtMain.setText(currentNumber);
evalExpression();
}
@OnClick(R.id.buttonKeyEqual)
public void onEqualsClicked() {
returnResult();
}
/*
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(this);
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 extractArguments() {
Intent intent = getIntent();
if (intent == null) return;
mCurrencyId = intent.getIntExtra(EXTRA_CURRENCY_ID, Constants.NOT_SET);
roundToCurrencyDecimals = intent.getBooleanExtra(EXTRA_ROUND_TO_CURRENCY, true);
String value = intent.getStringExtra(EXTRA_AMOUNT);
if (!TextUtils.isEmpty(value)) {
NumericHelper numericHelper = new NumericHelper(this);
Currency currency = mCurrencyService.getCurrency(mCurrencyId);
Money amount = MoneyFactory.fromString(value);
if (currency != null && this.roundToCurrencyDecimals) {
mAmount = numericHelper.truncateToCurrency(amount, currency);
} else {
// no currency and no base currency set.
mAmount = amount;
}
}
}
private void initializeControls() {
// set the decimal separator according to the locale
setDecimalSeparator();
// Numbers and Operators.
View.OnClickListener numberClickListener = new View.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) findViewById(id);
button.setOnClickListener(numberClickListener);
}
View.OnClickListener operatorClickListener = new View.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) findViewById(id);
button.setOnClickListener(operatorClickListener);
}
// Clear button. 'C'
Button clearButton = (Button) findViewById(R.id.buttonKeyClear);
clearButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
mStartedTyping = true;
txtMain.setText("");
evalExpression();
}
});
// delete button, <=
UIHelper uiHelper = new UIHelper(this);
deleteButton.setImageDrawable(uiHelper.getIcon(GoogleMaterial.Icon.gmd_backspace)
.sizeDp(40)
.color(uiHelper.getColor(R.color.md_primary)));
// Amounts
txtTop = (TextView) findViewById(R.id.textViewTop);
mDefaultColor = txtTop.getCurrentTextColor();
txtMain = (TextView) 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();
}
/**
* Set the decimal separator to the base currency's separator.
*/
private void setDecimalSeparator() {
Button decimalSeparatorButton = (Button) findViewById(R.id.buttonKeyNumDecimal);
String separator = this.formatUtilitiesLazy.get().getDecimalSeparatorForAppLocale();
decimalSeparatorButton.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(this);
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(roundToCurrencyDecimals) {
// use decimals from the display currency.
// but decimal and group separators from the base currency.
result = formatUtilitiesLazy.get().format(amount, displayCurrency.getScale(),
formatUtilitiesLazy.get().getDecimalSeparatorForAppLocale(),
formatUtilitiesLazy.get().getGroupingSeparatorForAppLocale());
} else {
// Use default precision and no currency markup.
result = formatUtilitiesLazy.get().formatNumber(amount, Constants.DEFAULT_PRECISION,
displayCurrency.getDecimalSeparator(), displayCurrency.getGroupSeparator(),
null, null);
}
} else {
result = formatUtilitiesLazy.get().formatWithLocale(amount);
}
return result;
}
private void returnResult() {
// this is called only to reset the warning colour in the top box, if any.
evalExpression();
showAmountInEntryField();
// set result and return
Intent result = new Intent();
result.putExtra(RESULT_AMOUNT, mAmount.toString());
setResult(Activity.RESULT_OK, result);
finish();
}
private void showAmountInEntryField() {
// Get the calculated amount in default locale and display in the main box.
String amount = getFormattedAmountForEditing(mAmount);
txtMain.setText(amount);
}
}