/******************************************************************************* * Copyright (c) 2010 Denis Solonenko. * All rights reserved. This program and the accompanying materials * are made available under the terms of the GNU Public License v2.0 * which accompanies this distribution, and is available at * http://www.gnu.org/licenses/old-licenses/gpl-2.0.html * * Contributors: * Denis Solonenko - initial API and implementation ******************************************************************************/ package ru.orangesoftware.financisto2.activity; import android.app.AlertDialog; import android.content.DialogInterface; import android.content.Intent; import android.os.Bundle; import android.util.Log; import android.view.View; import android.widget.AdapterView; import android.widget.LinearLayout; import android.widget.ListAdapter; import android.widget.TextView; import android.widget.Toast; import org.androidannotations.annotations.Bean; import org.androidannotations.annotations.EActivity; import org.androidannotations.annotations.Extra; import org.androidannotations.annotations.OptionsMenu; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.io.Serializable; import java.util.ArrayList; import java.util.IdentityHashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; import greendroid.widget.QuickAction; import greendroid.widget.QuickActionGrid; import greendroid.widget.QuickActionWidget; import ru.orangesoftware.financisto2.R; import ru.orangesoftware.financisto2.db.DatabaseHelper; import ru.orangesoftware.financisto2.model.Account; import ru.orangesoftware.financisto2.model.Category; import ru.orangesoftware.financisto2.model.Currency; import ru.orangesoftware.financisto2.model.MyEntity; import ru.orangesoftware.financisto2.model.Payee; import ru.orangesoftware.financisto2.model.Transaction; import ru.orangesoftware.financisto2.utils.CurrencyCache; import ru.orangesoftware.financisto2.utils.MyPreferences; import ru.orangesoftware.financisto2.utils.SplitAdjuster; import ru.orangesoftware.financisto2.utils.TransactionUtils; import ru.orangesoftware.financisto2.utils.Utils; import ru.orangesoftware.financisto2.widget.AmountInput; import static ru.orangesoftware.financisto2.utils.AndroidUtils.isGreenDroidSupported; import static ru.orangesoftware.financisto2.utils.Utils.isNotEmpty; @EActivity @OptionsMenu(R.menu.transaction_menu) public class TransactionActivity extends AbstractTransactionActivity { public static final String ACTIVITY_STATE = "ACTIVITY_STATE"; private static final int SPLIT_REQUEST = 5001; private final Currency currencyAsAccount = new Currency(); @Bean protected Utils u; @Extra protected boolean isUpdateBalanceMode = false; @Extra protected long currentBalance = 0; private long idSequence = 0; private final IdentityHashMap<View, Transaction> viewToSplitMap = new IdentityHashMap<View, Transaction>(); private TextView differenceText; private LinearLayout splitsLayout; private TextView unsplitAmountText; private TextView currencyText; private QuickActionWidget unsplitActionGrid; private long selectedOriginCurrencyId = -1; protected int getLayoutId() { return MyPreferences.isUseFixedLayout(this) ? R.layout.transaction_fixed : R.layout.transaction_free; } @Override protected void internalOnCreate() { if (transaction.isTemplateLike()) { setTitle(transaction.isTemplate() ? R.string.transaction_template : R.string.transaction_schedule); if (transaction.isTemplate()) { dateText.setEnabled(false); timeText.setEnabled(false); } } prepareUnsplitActionGrid(); currencyAsAccount.name = getString(R.string.original_currency_as_account); } private void prepareUnsplitActionGrid() { if (isGreenDroidSupported()) { unsplitActionGrid = new QuickActionGrid(this); unsplitActionGrid.addQuickAction(new QuickAction(this, R.drawable.ic_action_add, R.string.transaction)); unsplitActionGrid.addQuickAction(new QuickAction(this, R.drawable.ic_action_transfer_thin, R.string.transfer)); unsplitActionGrid.addQuickAction(new QuickAction(this, R.drawable.ic_action_share, R.string.unsplit_adjust_amount)); unsplitActionGrid.addQuickAction(new QuickAction(this, R.drawable.ic_action_share, R.string.unsplit_adjust_evenly)); unsplitActionGrid.addQuickAction(new QuickAction(this, R.drawable.ic_action_share, R.string.unsplit_adjust_last)); unsplitActionGrid.setOnQuickActionClickListener(unsplitActionListener); } } private QuickActionWidget.OnQuickActionClickListener unsplitActionListener = new QuickActionWidget.OnQuickActionClickListener() { public void onQuickActionClicked(QuickActionWidget widget, int position) { switch (position) { case 0: createSplit(false); break; case 1: createSplit(true); break; case 2: unsplitAdjustAmount(); break; case 3: unsplitAdjustEvenly(); break; case 4: unsplitAdjustLast(); break; } } }; private void unsplitAdjustAmount() { long splitAmount = calculateSplitAmount(); rateView.setFromAmount(splitAmount); updateUnsplitAmount(); } private void unsplitAdjustEvenly() { long unsplitAmount = calculateUnsplitAmount(); if (unsplitAmount != 0) { List<Transaction> splits = new ArrayList<Transaction>(viewToSplitMap.values()); SplitAdjuster.adjustEvenly(splits, unsplitAmount); updateSplits(); } } private void unsplitAdjustLast() { long unsplitAmount = calculateUnsplitAmount(); if (unsplitAmount != 0) { Transaction latestTransaction = null; for (Transaction t : viewToSplitMap.values()) { if (latestTransaction == null || latestTransaction.id > t.id) { latestTransaction = t; } } if (latestTransaction != null) { SplitAdjuster.adjustSplit(latestTransaction, unsplitAmount); updateSplits(); } } } private void updateSplits() { for (Map.Entry<View, Transaction> entry : viewToSplitMap.entrySet()) { View v = entry.getKey(); Transaction split = entry.getValue(); setSplitData(v, split); } updateUnsplitAmount(); } @Override protected void fetchCategories() { categorySelector = new CategorySelector(this, x, !isUpdateBalanceMode); categorySelector.setListener(this); categorySelector.fetchCategories(); } @Override protected void createListNodes(LinearLayout layout) { //account accountText = x.addListNode(layout, R.id.account, R.string.account, R.string.select_account); //payee isShowPayee = MyPreferences.isShowPayee(this); if (isShowPayee) { createPayeeNode(layout); payeeText.setOnItemClickListener(new AdapterView.OnItemClickListener() { @Override public void onItemClick(AdapterView<?> adapterView, View view, int i, long id) { if (isRememberLastCategory) { selectLastCategoryForPayee(id); } } }); } //category categorySelector.createNode(layout, true); //amount if (!isUpdateBalanceMode && MyPreferences.isShowCurrency(this)) { currencyText = x.addListNode(layout, R.id.original_currency, R.string.currency, R.string.original_currency_as_account); } else { currencyText = new TextView(this); } rateView.createTransactionUI(); // difference if (isUpdateBalanceMode) { differenceText = x.addInfoNode(layout, -1, R.string.difference, "0"); rateView.setFromAmount(currentBalance); rateView.setAmountFromChangeListener(new AmountInput.OnAmountChangedListener() { @Override public void onAmountChanged(long oldAmount, long newAmount) { long balanceDifference = newAmount - currentBalance; u.setAmountText(differenceText, rateView.getCurrencyFrom(), balanceDifference, true); } }); if (currentBalance > 0) { rateView.setIncome(); } else { rateView.setExpense(); } } else { if (currentBalance > 0) { rateView.setIncome(); } else { rateView.setExpense(); } createSplitsLayout(layout); rateView.setAmountFromChangeListener(new AmountInput.OnAmountChangedListener() { @Override public void onAmountChanged(long oldAmount, long newAmount) { updateUnsplitAmount(); } }); } } private void selectLastCategoryForPayee(long id) { Payee p = db.get(Payee.class, id); if (p != null) { categorySelector.selectCategory(p.lastCategoryId); } } private void createSplitsLayout(LinearLayout layout) { splitsLayout = new LinearLayout(this); splitsLayout.setOrientation(LinearLayout.VERTICAL); layout.addView(splitsLayout, new LinearLayout.LayoutParams(LinearLayout.LayoutParams.FILL_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT)); } @Override protected void addOrRemoveSplits() { if (splitsLayout == null) { return; } if (categorySelector.isSplitCategorySelected()) { View v = x.addNodeUnsplit(splitsLayout); unsplitAmountText = (TextView)v.findViewById(R.id.data); updateUnsplitAmount(); } else { splitsLayout.removeAllViews(); } } private void updateUnsplitAmount() { if (unsplitAmountText != null) { long amountDifference = calculateUnsplitAmount(); u.setAmountText(unsplitAmountText, rateView.getCurrencyFrom(), amountDifference, false); } } private long calculateUnsplitAmount() { long splitAmount = calculateSplitAmount(); return rateView.getFromAmount()-splitAmount; } private long calculateSplitAmount() { long amount = 0; for (Transaction split : viewToSplitMap.values()) { amount += split.fromAmount; } return amount; } protected void switchIncomeExpenseButton(Category category) { if (!isUpdateBalanceMode) { if (category.isIncome()) { rateView.setIncome(); } else { rateView.setExpense(); } } } @Override public void onWindowFocusChanged(boolean hasFocus) { if (hasFocus) { accountText.requestFocusFromTouch(); } } @Override protected boolean onOKClicked() { if (checkSelectedId(getSelectedAccountId(), R.string.select_account) && checkUnsplitAmount()) { updateTransactionFromUI(); return true; } return false; } private boolean checkUnsplitAmount() { if (categorySelector.isSplitCategorySelected()) { long unsplitAmount = calculateUnsplitAmount(); if (unsplitAmount != 0) { Toast.makeText(this, R.string.unsplit_amount_greater_than_zero, Toast.LENGTH_LONG).show(); return false; } } return true; } @Override protected void editTransaction(Transaction transaction) { selectAccount(transaction.fromAccountId, false); commonEditTransaction(transaction); selectCurrency(transaction); fetchSplits(); selectPayee(transaction.payeeId); } private void selectCurrency(Transaction transaction) { if (transaction.originalCurrencyId > 0) { selectOriginalCurrency(transaction.originalCurrencyId); rateView.setFromAmount(transaction.originalFromAmount); rateView.setToAmount(transaction.fromAmount); } else { rateView.setFromAmount(transaction.fromAmount); } } private void fetchSplits() { List<Transaction> splits = db.getSplitsForTransaction(transaction.id); for (Transaction split : splits) { split.categoryAttributes = db.getAllAttributesForTransaction(split.id); if (split.originalCurrencyId > 0) { split.fromAmount = split.originalFromAmount; } addOrEditSplit(split); } } private void updateTransactionFromUI() { updateTransactionFromUI(transaction); transaction.fromAccountId = selectedAccount.id; long amount = rateView.getFromAmount(); if (isUpdateBalanceMode) { amount -= currentBalance; } transaction.fromAmount = amount; updateTransactionOriginalAmount(); if (categorySelector.isSplitCategorySelected()) { transaction.splits = new LinkedList<Transaction>(viewToSplitMap.values()); } else { transaction.splits = null; } } private void updateTransactionOriginalAmount() { if (isDifferentCurrency()) { transaction.originalCurrencyId = selectedOriginCurrencyId; transaction.originalFromAmount = rateView.getFromAmount(); transaction.fromAmount = rateView.getToAmount(); } else { transaction.originalCurrencyId = 0; transaction.originalFromAmount = 0; } } private boolean isDifferentCurrency() { return selectedOriginCurrencyId > 0 && selectedOriginCurrencyId != selectedAccount.currency.id; } @Override protected Account selectAccount(long accountId, boolean selectLast) { Account a = super.selectAccount(accountId, selectLast); if (a != null) { if (selectLast && !isShowPayee && isRememberLastCategory) { categorySelector.selectCategory(a.lastCategoryId); } } if (selectedOriginCurrencyId > 0) { selectOriginalCurrency(selectedOriginCurrencyId); } return a; } @Override protected void onClick(View v, int id) { super.onClick(v, id); switch (id) { case R.id.unsplit_action: if (isGreenDroidSupported()) { unsplitActionGrid.show(v); } else { showQuickActionsDialog(); } break; case R.id.add_split: createSplit(false); break; case R.id.add_split_transfer: if (selectedOriginCurrencyId > 0) { Toast.makeText(this, R.string.split_transfer_not_supported_yet, Toast.LENGTH_LONG).show(); break; } createSplit(true); break; case R.id.delete_split: View parentView = (View)v.getParent(); deleteSplit(parentView); break; case R.id.original_currency: List<Currency> currencies = db.getAllCurrenciesList(); currencies.add(0, currencyAsAccount); ListAdapter adapter = TransactionUtils.createCurrencyAdapter(this, currencies); int selectedPos = MyEntity.indexOf(currencies, selectedOriginCurrencyId); x.selectItemId(this, R.id.currency, R.string.currency, adapter, selectedPos); break; } Transaction split = viewToSplitMap.get(v); if (split != null) { split.unsplitAmount = split.fromAmount + calculateUnsplitAmount(); editSplit(split, split.toAccountId > 0); } } @Override public void onSelectedId(int id, long selectedId) { super.onSelectedId(id, selectedId); switch (id) { case R.id.currency: selectOriginalCurrency(selectedId); break; } } private void selectOriginalCurrency(long selectedId) { selectedOriginCurrencyId = selectedId; if (selectedId == -1) { if (selectedAccount != null) { if (selectedAccount.currency.id == rateView.getCurrencyToId()) { rateView.setFromAmount(rateView.getToAmount()); } } selectAccountCurrency(); } else { long toAmount = rateView.getToAmount(); Currency currency = CurrencyCache.getCurrency(db, selectedId); rateView.selectCurrencyFrom(currency); if (selectedAccount != null) { if (selectedId == selectedAccount.currency.id) { if (selectedId == rateView.getCurrencyToId()) { rateView.setFromAmount(toAmount); } selectAccountCurrency(); return; } rateView.selectCurrencyTo(selectedAccount.currency); } currencyText.setText(currency.name); } } private void selectAccountCurrency() { rateView.selectSameCurrency(selectedAccount != null ? selectedAccount.currency : Currency.EMPTY); currencyText.setText(R.string.original_currency_as_account); } private void showQuickActionsDialog() { new AlertDialog.Builder(this) .setTitle(R.string.unsplit_amount) .setItems(R.array.unsplit_quick_action_items, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialogInterface, int i) { unsplitActionListener.onQuickActionClicked(unsplitActionGrid, i); } }) .show(); } private void createSplit(boolean asTransfer) { Transaction split = new Transaction(); split.id = --idSequence; split.fromAccountId = getSelectedAccountId(); split.fromAmount = split.unsplitAmount = calculateUnsplitAmount(); split.originalCurrencyId = selectedOriginCurrencyId; editSplit(split, asTransfer); } private void editSplit(Transaction split, boolean asTransfer) { if (asTransfer) { SplitTransferActivity_.intent(this).split(split).startForResult(SPLIT_REQUEST); } else { SplitTransactionActivity_.intent(this).split(split).startForResult(SPLIT_REQUEST); } } @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { super.onActivityResult(requestCode, resultCode, data); if (requestCode == SPLIT_REQUEST) { if (resultCode == RESULT_OK) { Transaction split = Transaction.fromIntentAsSplit(data); addOrEditSplit(split); } } } @Override protected void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); Log.d("Financisto", "onSaveInstanceState"); try { if (categorySelector.isSplitCategorySelected()) { Log.d("Financisto", "Saving splits..."); ActivityState state = new ActivityState(); state.categoryId = categorySelector.getSelectedCategoryId(); state.idSequence = idSequence; state.splits = new ArrayList<Transaction>(viewToSplitMap.values()); ByteArrayOutputStream s = new ByteArrayOutputStream(); try { ObjectOutputStream out = new ObjectOutputStream(s); out.writeObject(state); outState.putByteArray(ACTIVITY_STATE, s.toByteArray()); } finally { s.close(); } } } catch (IOException e) { Log.e("Financisto", "Unable to save state", e); } } @Override protected void onRestoreInstanceState(Bundle savedInstanceState) { super.onRestoreInstanceState(savedInstanceState); Log.d("Financisto", "onRestoreInstanceState"); byte[] bytes = savedInstanceState.getByteArray(ACTIVITY_STATE); if (bytes != null) { try { ByteArrayInputStream s = new ByteArrayInputStream(bytes); try { ObjectInputStream in = new ObjectInputStream(s); ActivityState state = (ActivityState) in.readObject(); if (state.categoryId == Category.SPLIT_CATEGORY_ID) { Log.d("Financisto", "Restoring splits..."); viewToSplitMap.clear(); splitsLayout.removeAllViews(); idSequence = state.idSequence; categorySelector.selectCategory(state.categoryId); for (Transaction split : state.splits) { addOrEditSplit(split); } } } finally { s.close(); } } catch (Exception e) { Log.e("Financisto", "Unable to restore state", e); } } } private void addOrEditSplit(Transaction split) { View v = findView(split); if (v == null) { v = x.addSplitNodeMinus(splitsLayout, R.id.edit_aplit, R.id.delete_split, R.string.split, ""); } setSplitData(v, split); viewToSplitMap.put(v, split); updateUnsplitAmount(); } private View findView(Transaction split) { for (Map.Entry<View, Transaction> entry : viewToSplitMap.entrySet()) { Transaction s = entry.getValue(); if (s.id == split.id) { return entry.getKey(); } } return null; } private void setSplitData(View v, Transaction split) { TextView label = (TextView)v.findViewById(R.id.label); TextView data = (TextView)v.findViewById(R.id.data); setSplitData(split, label, data); } private void setSplitData(Transaction split, TextView label, TextView data) { if (split.isTransfer()) { setSplitDataTransfer(split, label, data); } else { setSplitDataTransaction(split, label, data); } } private void setSplitDataTransaction(Transaction split, TextView label, TextView data) { label.setText(createSplitTransactionTitle(split)); Currency currency = getCurrency(); u.setAmountText(data, currency, split.fromAmount, false); } private String createSplitTransactionTitle(Transaction split) { StringBuilder sb = new StringBuilder(); Category category = categoryRepository.getCategoryById(split.categoryId); sb.append(category.title); if (isNotEmpty(split.note)) { sb.append(" (").append(split.note).append(")"); } return sb.toString(); } private void setSplitDataTransfer(Transaction split, TextView label, TextView data) { Account fromAccount = db.getAccount(split.fromAccountId); Account toAccount = db.getAccount(split.toAccountId); u.setTransferTitleText(label, fromAccount, toAccount); u.setTransferAmountText(data, fromAccount.currency, split.fromAmount, toAccount.currency, split.toAmount); } private void deleteSplit(View v) { Transaction split = viewToSplitMap.remove(v); if (split != null) { removeSplitView(v); updateUnsplitAmount(); if (split.remoteKey!=null) { db.writeDeleteLog(DatabaseHelper.TRANSACTION_TABLE, split.remoteKey); } } } private void removeSplitView(View v) { splitsLayout.removeView(v); View dividerView = (View)v.getTag(); if (dividerView != null) { splitsLayout.removeView(dividerView); } } private Currency getCurrency() { if (selectedOriginCurrencyId > 0) { return CurrencyCache.getCurrency(db, selectedOriginCurrencyId); } if (selectedAccount != null) { return selectedAccount.currency; } return Currency.EMPTY; } @Override protected void onDestroy() { Log.d("Financisto", "TransactionActivity.onDestroy"); if (payeeAdapter != null) { payeeAdapter.changeCursor(null); } super.onDestroy(); } private static class ActivityState implements Serializable { public long categoryId; public long idSequence; public List<Transaction> splits; } }