Java tutorial
/* This file is part of My Expenses. * My Expenses 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. * * My Expenses 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 My Expenses. If not, see <http://www.gnu.org/licenses/>. */ package org.totschnig.myexpenses.activity; import android.Manifest; import android.annotation.SuppressLint; import android.app.DatePickerDialog; import android.app.Dialog; import android.app.NotificationManager; import android.app.TimePickerDialog; import android.content.ContentResolver; import android.content.ContentUris; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; import android.content.pm.PackageManager; import android.database.ContentObserver; import android.database.Cursor; import android.database.MatrixCursor; import android.database.MergeCursor; import android.net.Uri; import android.os.Build; import android.os.Bundle; import android.os.Handler; import android.provider.MediaStore; import android.support.v4.app.ActivityCompat; import android.support.v4.app.FragmentManager; import android.support.v4.app.LoaderManager; import android.support.v4.content.ContextCompat; import android.support.v4.content.CursorLoader; import android.support.v4.content.Loader; import android.support.v4.view.MenuItemCompat; import android.support.v7.widget.PopupMenu; import android.text.Editable; import android.text.TextPaint; import android.text.TextUtils; import android.text.TextWatcher; import android.util.Log; import android.view.ContextThemeWrapper; import android.view.Menu; import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; import android.view.inputmethod.InputMethodManager; import android.widget.AdapterView; import android.widget.AdapterView.OnItemClickListener; import android.widget.AdapterView.OnItemSelectedListener; import android.widget.ArrayAdapter; import android.widget.AutoCompleteTextView; import android.widget.Button; import android.widget.DatePicker; import android.widget.EditText; import android.widget.FilterQueryProvider; import android.widget.FrameLayout; import android.widget.ImageView; import android.widget.Spinner; import android.widget.TableLayout; import android.widget.TextView; import android.widget.TimePicker; import android.widget.Toast; import android.widget.ToggleButton; import com.android.calendar.CalendarContractCompat; import com.android.calendar.CalendarContractCompat.Events; import com.squareup.picasso.Picasso; import org.totschnig.myexpenses.MyApplication; import org.totschnig.myexpenses.R; import org.totschnig.myexpenses.adapter.CrStatusAdapter; import org.totschnig.myexpenses.adapter.OperationTypeAdapter; import org.totschnig.myexpenses.adapter.RecurrenceAdapter; import org.totschnig.myexpenses.dialog.ConfirmationDialogFragment; import org.totschnig.myexpenses.dialog.ConfirmationDialogFragment.ConfirmationDialogListener; import org.totschnig.myexpenses.dialog.ContribInfoDialogFragment; import org.totschnig.myexpenses.dialog.MessageDialogFragment; import org.totschnig.myexpenses.fragment.DbWriteFragment; import org.totschnig.myexpenses.fragment.PlanMonthFragment; import org.totschnig.myexpenses.fragment.SplitPartList; import org.totschnig.myexpenses.fragment.TemplatesList; import org.totschnig.myexpenses.model.Account; import org.totschnig.myexpenses.model.AccountType; import org.totschnig.myexpenses.model.ContribFeature; import org.totschnig.myexpenses.model.Model; import org.totschnig.myexpenses.model.Money; import org.totschnig.myexpenses.model.Plan; import org.totschnig.myexpenses.model.SplitPartCategory; import org.totschnig.myexpenses.model.SplitPartTransfer; import org.totschnig.myexpenses.model.SplitTransaction; import org.totschnig.myexpenses.model.Template; import org.totschnig.myexpenses.model.Transaction; import org.totschnig.myexpenses.model.Transaction.CrStatus; import org.totschnig.myexpenses.model.Transfer; import org.totschnig.myexpenses.preference.PrefKey; import org.totschnig.myexpenses.preference.SharedPreferencesCompat; import org.totschnig.myexpenses.provider.TransactionProvider; import org.totschnig.myexpenses.task.TaskExecutionFragment; import org.totschnig.myexpenses.ui.AmountEditText; import org.totschnig.myexpenses.ui.SimpleCursorAdapter; import org.totschnig.myexpenses.ui.SimpleCursorAdapter.CursorToStringConverter; import org.totschnig.myexpenses.ui.SpinnerHelper; import org.totschnig.myexpenses.util.AcraHelper; import org.totschnig.myexpenses.util.FilterCursorWrapper; import org.totschnig.myexpenses.util.Utils; import org.totschnig.myexpenses.widget.AbstractWidget; import org.totschnig.myexpenses.widget.TemplateWidget; import java.io.File; import java.io.Serializable; import java.math.BigDecimal; import java.math.RoundingMode; import java.text.DateFormat; import java.util.ArrayList; import java.util.Calendar; import java.util.Currency; import java.util.Date; import java.util.List; import static org.totschnig.myexpenses.provider.DatabaseConstants.KEY_ACCOUNTID; import static org.totschnig.myexpenses.provider.DatabaseConstants.KEY_CATID; import static org.totschnig.myexpenses.provider.DatabaseConstants.KEY_CURRENCY; import static org.totschnig.myexpenses.provider.DatabaseConstants.KEY_DATE; import static org.totschnig.myexpenses.provider.DatabaseConstants.KEY_INSTANCEID; import static org.totschnig.myexpenses.provider.DatabaseConstants.KEY_IS_NUMBERED; import static org.totschnig.myexpenses.provider.DatabaseConstants.KEY_LABEL; import static org.totschnig.myexpenses.provider.DatabaseConstants.KEY_METHODID; import static org.totschnig.myexpenses.provider.DatabaseConstants.KEY_PARENTID; import static org.totschnig.myexpenses.provider.DatabaseConstants.KEY_PAYEEID; import static org.totschnig.myexpenses.provider.DatabaseConstants.KEY_PAYEE_NAME; import static org.totschnig.myexpenses.provider.DatabaseConstants.KEY_PAYEE_NAME_NORMALIZED; import static org.totschnig.myexpenses.provider.DatabaseConstants.KEY_ROWID; import static org.totschnig.myexpenses.provider.DatabaseConstants.KEY_TEMPLATEID; import static org.totschnig.myexpenses.provider.DatabaseConstants.KEY_TRANSFER_ACCOUNT; import static org.totschnig.myexpenses.provider.DatabaseConstants.STATUS_NONE; import static org.totschnig.myexpenses.provider.DatabaseConstants.STATUS_UNCOMMITTED; import static org.totschnig.myexpenses.provider.DatabaseConstants.TABLE_PAYEES; import static org.totschnig.myexpenses.provider.DatabaseConstants.TABLE_TRANSACTIONS; import static org.totschnig.myexpenses.provider.DatabaseConstants.WHERE_NOT_SPLIT; /** * Activity for editing a transaction * * @author Michael Totschnig */ public class ExpenseEdit extends AmountActivity implements OnItemSelectedListener, LoaderManager.LoaderCallbacks<Cursor>, ContribIFace, ConfirmationDialogListener { private static final String SPLIT_PART_LIST = "SPLIT_PART_LIST"; public static final String KEY_NEW_TEMPLATE = "newTemplate"; public static final String KEY_CLONE = "clone"; private static final String KEY_CALENDAR = "calendar"; private static final String KEY_CACHED_DATA = "cachedData"; private static final String KEY_CACHED_RECURRENCE = "cachedRecurrence"; private static final String KEY_CACHED_PICTURE_URI = "cachedPictureUri"; private static final String PREFKEY_TRANSACTION_LAST_ACCOUNT_FROM_WIDGET = "transactionLastAccountFromWidget"; private static final String PREFKEY_TRANSFER_LAST_ACCOUNT_FROM_WIDGET = "transferLastAccountFromWidget"; private static final String PREFKEY_TRANSFER_LAST_TRANSFER_ACCOUNT_FROM_WIDGET = "transferLastTransferAccountFromWidget"; private static final String PREFKEY_SPLIT_LAST_ACCOUNT_FROM_WIDGET = "splitLastAccountFromWidget"; public static final int EXCHANGE_RATE_FRACTION_DIGITS = 5; private static final BigDecimal nullValue = new BigDecimal(0); private Button mDateButton; private Button mTimeButton; private EditText mCommentText, mTitleText, mReferenceNumberText; private AmountEditText mTransferAmountText, mExchangeRate1Text, mExchangeRate2Text; private Button mCategoryButton, mPlanButton; private Spinner mMethodSpinner; private SpinnerHelper mAccountSpinner, mTransferAccountSpinner, mStatusSpinner, mOperationTypeSpinner, mReccurenceSpinner; private SimpleCursorAdapter mMethodsAdapter, mAccountsAdapter, mTransferAccountsAdapter, mPayeeAdapter; private OperationTypeAdapter mOperationTypeAdapter; private FilterCursorWrapper mTransferAccountCursor; private AutoCompleteTextView mPayeeText; protected TextView mPayeeLabel; private ToggleButton mPlanToggleButton; private ImageView mAttachPictureButton; private FrameLayout mPictureViewContainer; public Long mRowId = 0L; private Long mTemplateId; private Account[] mAccounts; private Calendar mCalendar = Calendar.getInstance(); private DateFormat mDateFormat, mTimeFormat; private Long mCatId = null, mMethodId = null, mAccountId = null, mTransferAccountId; private String mLabel; private Transaction mTransaction; private Cursor mMethodsCursor; private Plan mPlan; private Uri mPictureUri, mPictureUriTemp; private long mPlanInstanceId, mPlanInstanceDate; /** * transaction, transfer or split */ private int mOperationType; static final int DATE_DIALOG_ID = 0; static final int TIME_DIALOG_ID = 1; public static final int METHODS_CURSOR = 2; public static final int ACCOUNTS_CURSOR = 3; public static final int TRANSACTION_CURSOR = 5; public static final int SUM_CURSOR = 6; public static final int LAST_EXCHANGE_CURSOR = 7; private static final String KEY_PICTURE_URI = "picture_uri"; private static final String KEY_PICTURE_URI_TMP = "picture_uri_tmp"; private LoaderManager mManager; protected boolean mClone = false; protected boolean mCreateNew; protected boolean mIsMainTransactionOrTemplate; protected boolean mSavedInstance; protected boolean mRecordTemplateWidget; private boolean mIsResumed; boolean isProcessingLinkedAmountInputs = false; private ContentObserver pObserver; private boolean mPlanUpdateNeeded; public enum HelpVariant { transaction, transfer, split, templateCategory, templateTransfer, splitPartCategory, splitPartTransfer } @Override int getDiscardNewMessage() { return mTransaction instanceof Template ? R.string.dialog_confirm_discard_new_template : R.string.dialog_confirm_discard_new_transaction; } @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.one_expense); mDateFormat = android.text.format.DateFormat.getDateFormat(this); mTimeFormat = android.text.format.DateFormat.getTimeFormat(this); setupToolbar(); mManager = getSupportLoaderManager(); //we enable it only after accountcursor has been loaded, preventing NPE when user clicks on it early configTypeButton(); mTypeButton.setEnabled(false); mCommentText = (EditText) findViewById(R.id.Comment); mTitleText = (EditText) findViewById(R.id.Title); mReferenceNumberText = (EditText) findViewById(R.id.Number); mDateButton = (Button) findViewById(R.id.DateButton); mAttachPictureButton = (ImageView) findViewById(R.id.AttachImage); mPictureViewContainer = (FrameLayout) findViewById(R.id.picture_container); mTimeButton = (Button) findViewById(R.id.TimeButton); mPayeeLabel = (TextView) findViewById(R.id.PayeeLabel); mPayeeText = (AutoCompleteTextView) findViewById(R.id.Payee); mTransferAmountText = (AmountEditText) findViewById(R.id.TranferAmount); mExchangeRate1Text = (AmountEditText) findViewById(R.id.ExchangeRate_1); mExchangeRate1Text.setFractionDigits(EXCHANGE_RATE_FRACTION_DIGITS); mExchangeRate1Text.addTextChangedListener(new LinkedExchangeRateTextWatchter(true)); mExchangeRate2Text = (AmountEditText) findViewById(R.id.ExchangeRate_2); mExchangeRate2Text.setFractionDigits(EXCHANGE_RATE_FRACTION_DIGITS); mExchangeRate2Text.addTextChangedListener(new LinkedExchangeRateTextWatchter(false)); mPayeeAdapter = new SimpleCursorAdapter(this, R.layout.support_simple_spinner_dropdown_item, null, new String[] { KEY_PAYEE_NAME }, new int[] { android.R.id.text1 }, 0); mPayeeText.setAdapter(mPayeeAdapter); mPayeeAdapter.setFilterQueryProvider(new FilterQueryProvider() { @SuppressLint("NewApi") public Cursor runQuery(CharSequence str) { if (str == null) { return null; } String search = Utils.esacapeSqlLikeExpression(Utils.normalize(str.toString())); //we accept the string at the beginning of a word String selection = KEY_PAYEE_NAME_NORMALIZED + " LIKE ? OR " + KEY_PAYEE_NAME_NORMALIZED + " LIKE ? OR " + KEY_PAYEE_NAME_NORMALIZED + " LIKE ?"; String[] selectArgs = { search + "%", "% " + search + "%", "%." + search + "%" }; return getContentResolver().query(TransactionProvider.PAYEES_URI, new String[] { KEY_ROWID, KEY_PAYEE_NAME, "(SELECT max(" + KEY_ROWID + ") FROM " + TABLE_TRANSACTIONS + " WHERE " + WHERE_NOT_SPLIT + " AND " + KEY_PAYEEID + " = " + TABLE_PAYEES + "." + KEY_ROWID + ")" }, selection, selectArgs, null); } }); mPayeeAdapter.setCursorToStringConverter(new CursorToStringConverter() { public CharSequence convertToString(Cursor cur) { return cur.getString(1); } }); mPayeeText.setOnItemClickListener(new OnItemClickListener() { @Override public void onItemClick(AdapterView<?> parent, View view, int position, long id) { Cursor c = (Cursor) mPayeeAdapter.getItem(position); if (c.moveToPosition(position)) { mTransaction.updatePayeeWithId(c.getString(1), c.getLong(0)); if (mNewInstance && mTransaction != null && !(mTransaction instanceof Template || mTransaction instanceof SplitTransaction)) { //moveToPosition should not be necessary, //but has been reported to not be positioned correctly on samsung GT-I8190N if (!c.isNull(2)) { if (PrefKey.AUTO_FILL_HINT_SHOWN.getBoolean(false)) { if (PrefKey.AUTO_FILL.getBoolean(true)) { startAutoFill(c.getLong(2)); } } else { Bundle b = new Bundle(); b.putLong(KEY_ROWID, c.getLong(2)); b.putInt(ConfirmationDialogFragment.KEY_TITLE, R.string.dialog_title_information); b.putString(ConfirmationDialogFragment.KEY_MESSAGE, getString(R.string.hint_auto_fill)); b.putInt(ConfirmationDialogFragment.KEY_COMMAND_POSITIVE, R.id.AUTO_FILL_COMMAND); b.putString(ConfirmationDialogFragment.KEY_PREFKEY, PrefKey.AUTO_FILL_HINT_SHOWN.getKey()); b.putInt(ConfirmationDialogFragment.KEY_POSITIVE_BUTTON_LABEL, R.string.yes); b.putInt(ConfirmationDialogFragment.KEY_NEGATIVE_BUTTON_LABEL, R.string.no); ConfirmationDialogFragment.newInstance(b).show(getSupportFragmentManager(), "AUTO_FILL_HINT"); } } } } } }); mCategoryButton = (Button) findViewById(R.id.Category); mPlanButton = (Button) findViewById(R.id.Plan); mMethodSpinner = (Spinner) findViewById(R.id.Method); mAccountSpinner = new SpinnerHelper(findViewById(R.id.Account)); mTransferAccountSpinner = new SpinnerHelper(findViewById(R.id.TransferAccount)); mTransferAccountSpinner.setOnItemSelectedListener(this); mStatusSpinner = new SpinnerHelper(findViewById(R.id.Status)); mReccurenceSpinner = new SpinnerHelper(findViewById(R.id.Recurrence)); mPlanToggleButton = (ToggleButton) findViewById(R.id.PlanExecutionAutomatic); TextPaint paint = mPlanToggleButton.getPaint(); int automatic = (int) paint.measureText(getString(R.string.plan_automatic)); int manual = (int) paint.measureText(getString(R.string.plan_manual)); mPlanToggleButton.setWidth((automatic > manual ? automatic : manual) + +mPlanToggleButton.getPaddingLeft() + mPlanToggleButton.getPaddingRight()); mRowId = Utils.getFromExtra(getIntent().getExtras(), KEY_ROWID, 0); //upon orientation change stored in instance state, since new splitTransactions are immediately persisted to DB if (savedInstanceState != null) { mSavedInstance = true; mRowId = savedInstanceState.getLong(KEY_ROWID); mPictureUri = savedInstanceState.getParcelable(KEY_PICTURE_URI); mPictureUriTemp = savedInstanceState.getParcelable(KEY_PICTURE_URI_TMP); setPicture(); mCalendar = (Calendar) savedInstanceState.getSerializable(KEY_CALENDAR); mLabel = savedInstanceState.getString(KEY_LABEL); if ((mCatId = savedInstanceState.getLong(KEY_CATID)) == 0L) { mCatId = null; } if ((mMethodId = savedInstanceState.getLong(KEY_METHODID)) == 0L) mMethodId = null; if ((mAccountId = savedInstanceState.getLong(KEY_ACCOUNTID)) == 0L) { mAccountId = null; } else { //once user has selected account, we no longer want //the passed in KEY_CURRENCY to override it in onLoadFinished getIntent().removeExtra(KEY_CURRENCY); } if ((mTransferAccountId = savedInstanceState.getLong(KEY_TRANSFER_ACCOUNT)) == 0L) mTransferAccountId = null; } mTemplateId = getIntent().getLongExtra(KEY_TEMPLATEID, 0); //were we called from a notification int notificationId = getIntent().getIntExtra(MyApplication.KEY_NOTIFICATION_ID, 0); if (notificationId > 0) { ((NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE)).cancel(notificationId); } CrStatusAdapter sAdapter = new CrStatusAdapter(this) { @Override public boolean isEnabled(int position) { //if the transaction is reconciled, the status can not be changed //otherwise only unreconciled and cleared can be set return mTransaction != null && mTransaction.crStatus != CrStatus.RECONCILED && position != CrStatus.RECONCILED.ordinal(); } }; mStatusSpinner.setAdapter(sAdapter); //1. fetch the transaction or create a new instance if (mRowId != 0 || mTemplateId != 0) { mNewInstance = false; int taskId; Serializable extra = null; Long objectId; if (mRowId != 0) { taskId = TaskExecutionFragment.TASK_INSTANTIATE_TRANSACTION; //if called with extra KEY_CLONE, we ask the task to clone, but no longer after orientation change extra = getIntent().getBooleanExtra(KEY_CLONE, false) && savedInstanceState == null; objectId = mRowId; } else { objectId = mTemplateId; //are we editing the template or instantiating a new one if ((mPlanInstanceId = getIntent().getLongExtra(KEY_INSTANCEID, 0)) != 0L) { taskId = TaskExecutionFragment.TASK_INSTANTIATE_TRANSACTION_FROM_TEMPLATE; mPlanInstanceDate = getIntent().getLongExtra(KEY_DATE, 0); mRecordTemplateWidget = getIntent().getBooleanExtra(AbstractWidget.EXTRA_START_FROM_WIDGET, false) && !ContribFeature.TEMPLATE_WIDGET.hasAccess(); } else { taskId = TaskExecutionFragment.TASK_INSTANTIATE_TEMPLATE; } } FragmentManager fm = getSupportFragmentManager(); if (fm.findFragmentByTag(ProtectionDelegate.ASYNC_TAG) == null) { startTaskExecution(taskId, new Long[] { objectId }, extra, R.string.progress_dialog_loading); } } else { mOperationType = getIntent().getIntExtra(MyApplication.KEY_OPERATION_TYPE, MyExpenses.TYPE_TRANSACTION); if (!isValidType(mOperationType)) { mOperationType = MyExpenses.TYPE_TRANSACTION; } if (mOperationType == MyExpenses.TYPE_SPLIT && !ContribFeature.SPLIT_TRANSACTION.hasAccess() && ContribFeature.SPLIT_TRANSACTION.usagesLeft() < 1) { Toast.makeText(this, ContribFeature.SPLIT_TRANSACTION.buildRequiresString(this), Toast.LENGTH_LONG) .show(); finish(); return; } final Long parentId = getIntent().getLongExtra(KEY_PARENTID, 0); final boolean isNewTemplate = getIntent().getBooleanExtra(KEY_NEW_TEMPLATE, false); getSupportActionBar().setDisplayShowTitleEnabled(false); View spinner = findViewById(R.id.OperationType); mOperationTypeSpinner = new SpinnerHelper(spinner); spinner.setVisibility(View.VISIBLE); List<Integer> allowedOperationTypes = new ArrayList<>(); allowedOperationTypes.add(MyExpenses.TYPE_TRANSACTION); allowedOperationTypes.add(MyExpenses.TYPE_TRANSFER); if (!isNewTemplate && parentId == 0) { allowedOperationTypes.add(MyExpenses.TYPE_SPLIT); } mOperationTypeAdapter = new OperationTypeAdapter(this, allowedOperationTypes, isNewTemplate, parentId != 0); mOperationTypeSpinner.setAdapter(mOperationTypeAdapter); resetOperationType(); mOperationTypeSpinner.setOnItemSelectedListener(this); Long accountId = getIntent().getLongExtra(KEY_ACCOUNTID, 0); if (isNewTemplate) { mTransaction = Template.getTypedNewInstance(mOperationType, accountId); } else { switch (mOperationType) { case MyExpenses.TYPE_TRANSACTION: if (accountId == 0L) { accountId = MyApplication.getInstance().getSettings() .getLong(PREFKEY_TRANSACTION_LAST_ACCOUNT_FROM_WIDGET, 0L); } mTransaction = parentId == 0L ? Transaction.getNewInstance(accountId) : SplitPartCategory.getNewInstance(accountId, parentId); break; case MyExpenses.TYPE_TRANSFER: Long transfer_account = 0L; if (accountId == 0L) { accountId = MyApplication.getInstance().getSettings() .getLong(PREFKEY_TRANSFER_LAST_ACCOUNT_FROM_WIDGET, 0L); transfer_account = MyApplication.getInstance().getSettings() .getLong(PREFKEY_TRANSFER_LAST_TRANSFER_ACCOUNT_FROM_WIDGET, 0L); } mTransaction = parentId == 0L ? Transfer.getNewInstance(accountId, transfer_account) : SplitPartTransfer.getNewInstance(accountId, parentId, transfer_account); break; case MyExpenses.TYPE_SPLIT: if (accountId == 0L) { accountId = MyApplication.getInstance().getSettings() .getLong(PREFKEY_SPLIT_LAST_ACCOUNT_FROM_WIDGET, 0L); } mTransaction = SplitTransaction.getNewInstance(accountId); //Split transactions are returned persisted to db and already have an id if (mTransaction != null) { mRowId = mTransaction.getId(); } break; } } if (mTransaction == null) { String errMsg = "Error instantiating transaction for account " + accountId; AcraHelper.report(new IllegalStateException(errMsg), "Extras", getIntent().getExtras().toString()); Toast.makeText(this, errMsg, Toast.LENGTH_SHORT).show(); finish(); return; } if (!mSavedInstance) { //processing data from user switching operation type Transaction cached = (Transaction) getIntent().getSerializableExtra(KEY_CACHED_DATA); if (cached != null) { mTransaction.accountId = cached.accountId; mCalendar.setTime(cached.getDate()); mPictureUri = getIntent().getParcelableExtra(KEY_CACHED_PICTURE_URI); setPicture(); mTransaction.methodId = cached.methodId; } } setup(); } } @Override protected void onResume() { super.onResume(); mIsResumed = true; if (mAccounts != null) setupListeners(); } @Override protected void onDestroy() { super.onDestroy(); if (pObserver != null) { try { ContentResolver cr = getContentResolver(); cr.unregisterContentObserver(pObserver); } catch (IllegalStateException ise) { // Do Nothing. Observer has already been unregistered. } } } private void setup() { mAmountText.setFractionDigits(Money.getFractionDigits(mTransaction.getAmount().getCurrency())); linkInputsWithLabels(); if (mTransaction instanceof SplitTransaction) { mAmountText.addTextChangedListener(new MyTextWatcher() { @Override public void afterTextChanged(Editable s) { findSplitPartList().updateBalance(); } }); } if (mOperationType == MyExpenses.TYPE_TRANSFER) { mAmountText.addTextChangedListener(new LinkedTransferAmountTextWatcher(true)); mTransferAmountText.addTextChangedListener(new LinkedTransferAmountTextWatcher(false)); } // Spinner for account and transfer account mAccountsAdapter = new SimpleCursorAdapter(this, android.R.layout.simple_spinner_item, null, new String[] { KEY_LABEL }, new int[] { android.R.id.text1 }, 0); mAccountsAdapter.setDropDownViewResource(R.layout.support_simple_spinner_dropdown_item); mAccountSpinner.setAdapter(mAccountsAdapter); if (mTransaction instanceof SplitPartCategory || mTransaction instanceof SplitPartTransfer) { disableAccountSpinner(); } mIsMainTransactionOrTemplate = mOperationType != MyExpenses.TYPE_TRANSFER && !(mTransaction instanceof SplitPartCategory); if (mIsMainTransactionOrTemplate) { // Spinner for methods mMethodsAdapter = new SimpleCursorAdapter(this, android.R.layout.simple_spinner_item, null, new String[] { KEY_LABEL }, new int[] { android.R.id.text1 }, 0); mMethodsAdapter.setDropDownViewResource(R.layout.support_simple_spinner_dropdown_item); mMethodSpinner.setAdapter(mMethodsAdapter); } else { findViewById(R.id.PayeeRow).setVisibility(View.GONE); View MethodContainer = findViewById(R.id.MethodRow); MethodContainer.setVisibility(View.GONE); } View categoryContainer = findViewById(R.id.CategoryRow); if (categoryContainer == null) categoryContainer = findViewById(R.id.Category); TextView accountLabelTv = (TextView) findViewById(R.id.AccountLabel); if (mOperationType == MyExpenses.TYPE_TRANSFER) { mTypeButton.setVisibility(View.GONE); categoryContainer.setVisibility(View.GONE); View accountContainer = findViewById(R.id.TransferAccountRow); if (accountContainer == null) accountContainer = findViewById(R.id.TransferAccount); accountContainer.setVisibility(View.VISIBLE); accountLabelTv.setText(R.string.transfer_from_account); mTransferAccountsAdapter = new SimpleCursorAdapter(this, android.R.layout.simple_spinner_item, null, new String[] { KEY_LABEL }, new int[] { android.R.id.text1 }, 0); mTransferAccountsAdapter.setDropDownViewResource(R.layout.support_simple_spinner_dropdown_item); mTransferAccountSpinner.setAdapter(mTransferAccountsAdapter); } mManager.initLoader(ACCOUNTS_CURSOR, null, this); if (mTransaction instanceof Template) { findViewById(R.id.TitleRow).setVisibility(View.VISIBLE); if (!calendarPermissionPermanentlyDeclined()) { //if user has denied access and checked that he does not want to be asked again, we do not //bother him with a button that is not working setPlannerRowVisibility(View.VISIBLE); RecurrenceAdapter recurrenceAdapter = new RecurrenceAdapter(this, false); mReccurenceSpinner.setAdapter(recurrenceAdapter); mReccurenceSpinner.setOnItemSelectedListener(this); mPlanButton.setOnClickListener(new View.OnClickListener() { public void onClick(View view) { if (mPlan == null) { showDialog(DATE_DIALOG_ID); } else if (Utils.IS_ANDROID) { launchPlanView(); } } }); } mAttachPictureButton.setVisibility(View.GONE); setTitle(getString( mTransaction.getId() == 0 ? R.string.menu_create_template : R.string.menu_edit_template) + " (" + getString( mOperationType == MyExpenses.TYPE_TRANSFER ? R.string.transfer : R.string.transaction) + ")"); helpVariant = mOperationType == MyExpenses.TYPE_TRANSFER ? HelpVariant.templateTransfer : HelpVariant.templateCategory; } else if (mTransaction instanceof SplitTransaction) { setTitle(mNewInstance ? R.string.menu_create_split : R.string.menu_edit_split); //SplitTransaction are always instantiated with status uncommitted, //we save them to DB as uncommitted, before working with them //when the split transaction is saved the split and its parts are committed categoryContainer.setVisibility(View.GONE); //add split list if (findSplitPartList() == null) { FragmentManager fm = getSupportFragmentManager(); fm.beginTransaction().add(R.id.OneExpense, SplitPartList.newInstance(mTransaction.getId(), mTransaction.accountId), SPLIT_PART_LIST) .commit(); fm.executePendingTransactions(); } helpVariant = HelpVariant.split; } else { if (mTransaction instanceof SplitPartCategory) { setTitle(mTransaction.getId() == 0 ? R.string.menu_create_split_part_category : R.string.menu_edit_split_part_category); helpVariant = HelpVariant.splitPartCategory; mTransaction.status = STATUS_UNCOMMITTED; } else if (mTransaction instanceof SplitPartTransfer) { setTitle(mTransaction.getId() == 0 ? R.string.menu_create_split_part_transfer : R.string.menu_edit_split_part_transfer); helpVariant = HelpVariant.splitPartTransfer; mTransaction.status = STATUS_UNCOMMITTED; } else { //Transfer or Template, we can suggest to create a plan if (!calendarPermissionPermanentlyDeclined()) { //we set adapter even if spinner is not immediately visible, since it might become visible //after SAVE_AND_NEW action RecurrenceAdapter recurrenceAdapter = new RecurrenceAdapter(this, true); mReccurenceSpinner.setAdapter(recurrenceAdapter); Plan.Recurrence cachedRecurrence = (Plan.Recurrence) getIntent() .getSerializableExtra(KEY_CACHED_RECURRENCE); if (cachedRecurrence != null) { mReccurenceSpinner.setSelection( ((ArrayAdapter) mReccurenceSpinner.getAdapter()).getPosition(cachedRecurrence)); } mReccurenceSpinner.setOnItemSelectedListener(this); findViewById(R.id.PlannerRow).setVisibility(View.VISIBLE); if (mTransaction.originTemplate != null && mTransaction.originTemplate.getPlan() != null) { mReccurenceSpinner.getSpinner().setVisibility(View.GONE); mPlanButton.setVisibility(View.VISIBLE); mPlanButton.setText(Plan.prettyTimeInfo(this, mTransaction.originTemplate.getPlan().rrule, mTransaction.originTemplate.getPlan().dtstart)); mPlanButton.setOnClickListener(new View.OnClickListener() { public void onClick(View view) { PlanMonthFragment .newInstance(mTransaction.originTemplate.getTitle(), mTransaction.originTemplate.getId(), mTransaction.originTemplate.planId, getCurrentAccount().color, true) .show(getSupportFragmentManager(), TemplatesList.CALDROID_DIALOG_FRAGMENT_TAG); } }); } } if (mTransaction instanceof Transfer) { setTitle(mTransaction.getId() == 0 ? R.string.menu_create_transfer : R.string.menu_edit_transfer); helpVariant = HelpVariant.transfer; } else if (mTransaction instanceof Transaction) { setTitle(mTransaction.getId() == 0 ? R.string.menu_create_transaction : R.string.menu_edit_transaction); helpVariant = HelpVariant.transaction; } } } if (mClone) { setTitle(R.string.menu_clone_transaction); } if (mTransaction instanceof Template || mTransaction instanceof SplitPartCategory || mTransaction instanceof SplitPartTransfer) { findViewById(R.id.DateTimeRow).setVisibility(View.GONE); } else { //noinspection SetTextI18n ((TextView) findViewById(R.id.DateTimeLabel)) .setText(getString(R.string.date) + " / " + getString(R.string.time)); mDateButton.setOnClickListener(new View.OnClickListener() { public void onClick(View v) { showDialog(DATE_DIALOG_ID); } }); mTimeButton.setOnClickListener(new View.OnClickListener() { public void onClick(View v) { showDialog(TIME_DIALOG_ID); } }); } //when we have a savedInstance, fields have already been populated if (!mSavedInstance) { populateFields(); } if (!(mTransaction instanceof SplitPartCategory || mTransaction instanceof SplitPartTransfer)) { setDateTime(); } //after setdatetime, so that the plan info can override the date configurePlan(); if (mType == INCOME && mOperationType == MyExpenses.TYPE_TRANSFER) { switchAccountViews(); } setCategoryButton(); if (mOperationType != MyExpenses.TYPE_TRANSFER) { mCategoryButton.setOnClickListener(new View.OnClickListener() { public void onClick(View view) { startSelectCategory(); } }); } } private void setPlannerRowVisibility(int visibility) { findViewById(R.id.PlannerRow).setVisibility(visibility); } @Override protected void setupListeners() { super.setupListeners(); mCommentText.addTextChangedListener(this); mTitleText.addTextChangedListener(this); mPayeeText.addTextChangedListener(this); mReferenceNumberText.addTextChangedListener(this); mAccountSpinner.setOnItemSelectedListener(this); mMethodSpinner.setOnItemSelectedListener(this); mStatusSpinner.setOnItemSelectedListener(this); } @Override protected void linkInputsWithLabels() { super.linkInputsWithLabels(); linkAccountLabels(); linkInputWithLabel(mTitleText, findViewById(R.id.TitleLabel)); linkInputWithLabel(mDateButton, findViewById(R.id.DateTimeLabel)); linkInputWithLabel(mTimeButton, findViewById(R.id.DateTimeLabel)); linkInputWithLabel(mPayeeText, mPayeeLabel); View commentLabel = findViewById(R.id.CommentLabel); linkInputWithLabel(mStatusSpinner.getSpinner(), commentLabel); linkInputWithLabel(mAttachPictureButton, commentLabel); linkInputWithLabel(mPictureViewContainer, commentLabel); linkInputWithLabel(mCommentText, commentLabel); linkInputWithLabel(mCategoryButton, findViewById(R.id.CategoryLabel)); View methodLabel = findViewById(R.id.MethodLabel); linkInputWithLabel(mMethodSpinner, methodLabel); linkInputWithLabel(mReferenceNumberText, methodLabel); View planLabel = findViewById(R.id.PlanLabel); linkInputWithLabel(mPlanButton, planLabel); linkInputWithLabel(mReccurenceSpinner.getSpinner(), planLabel); linkInputWithLabel(mPlanToggleButton, planLabel); final View transferAmountLabel = findViewById(R.id.TransferAmountLabel); linkInputWithLabel(mTransferAmountText, transferAmountLabel); linkInputWithLabel(findViewById(R.id.CalculatorTransfer), transferAmountLabel); final View exchangeRateAmountLabel = findViewById(R.id.ExchangeRateLabel); linkInputWithLabel(findViewById(R.id.ExchangeRate_1), exchangeRateAmountLabel); linkInputWithLabel(findViewById(R.id.ExchangeRate_2), exchangeRateAmountLabel); } private void linkAccountLabels() { final View accountLabel = findViewById(R.id.AccountLabel); final View transferAccountLabel = findViewById(R.id.TransferAccountLabel); linkInputWithLabel(mAccountSpinner.getSpinner(), mType == INCOME ? transferAccountLabel : accountLabel); linkInputWithLabel(mTransferAccountSpinner.getSpinner(), mType == INCOME ? accountLabel : transferAccountLabel); } @Override protected void onTypeChanged(boolean isClicked) { super.onTypeChanged(isClicked); if (mTransaction != null && mIsMainTransactionOrTemplate) { mTransaction.methodId = null; if (mManager.getLoader(METHODS_CURSOR) != null && !mManager.getLoader(METHODS_CURSOR).isReset()) { mManager.restartLoader(METHODS_CURSOR, null, this); } else { mManager.initLoader(METHODS_CURSOR, null, this); } } } @Override public boolean onCreateOptionsMenu(Menu menu) { super.onCreateOptionsMenu(menu); if (!(mTransaction instanceof SplitPartCategory || mTransaction instanceof SplitPartTransfer || mTransaction instanceof Template || (mTransaction instanceof SplitTransaction && !MyApplication.getInstance().getLicenceHandler().isContribEnabled()))) { MenuItemCompat .setShowAsAction(menu.add(Menu.NONE, R.id.SAVE_AND_NEW_COMMAND, 0, R.string.menu_save_and_new) .setIcon(R.drawable.ic_action_save_new), MenuItemCompat.SHOW_AS_ACTION_ALWAYS); } if (mOperationType == MyExpenses.TYPE_TRANSFER) { MenuItemCompat.setShowAsAction( menu.add(Menu.NONE, R.id.INVERT_TRANSFER_COMMAND, 0, R.string.menu_invert_transfer) .setIcon(R.drawable.ic_menu_move), MenuItemCompat.SHOW_AS_ACTION_ALWAYS); } return true; } @Override public boolean dispatchCommand(int command, Object tag) { switch (command) { case android.R.id.home: cleanup(); finish(); return true; case R.id.SAVE_COMMAND: case R.id.SAVE_AND_NEW_COMMAND: if (mTransaction instanceof SplitTransaction && !findSplitPartList().splitComplete()) { Toast.makeText(this, getString(R.string.unsplit_amount_greater_than_zero), Toast.LENGTH_SHORT) .show(); return true; } if (command == R.id.SAVE_COMMAND) { //handled in super break; } if (!mIsSaving) { mCreateNew = true; saveState(); } return true; case R.id.CREATE_COMMAND: createRow(); return true; case R.id.INVERT_TRANSFER_COMMAND: mType = !mType; switchAccountViews(); } return super.dispatchCommand(command, tag); } private boolean checkTransferEnabled(Account account) { if (account == null) return false; if (!(mAccounts.length > 1)) { MessageDialogFragment .newInstance(0, getString(R.string.dialog_command_disabled_insert_transfer), MessageDialogFragment.Button.okButton(), null, null) .show(getSupportFragmentManager(), "BUTTON_DISABLED_INFO"); return false; } return true; } private void createRow() { Account account = getCurrentAccount(); if (account == null) { Toast.makeText(this, R.string.account_list_not_yet_loaded, Toast.LENGTH_LONG).show(); return; } Intent i = new Intent(this, ExpenseEdit.class); forwardDataEntryFromWidget(i); i.putExtra(MyApplication.KEY_OPERATION_TYPE, MyExpenses.TYPE_TRANSACTION); i.putExtra(KEY_ACCOUNTID, account.getId()); i.putExtra(KEY_PARENTID, mTransaction.getId()); startActivityForResult(i, EDIT_SPLIT_REQUEST); } /** * calls the activity for selecting (and managing) categories */ private void startSelectCategory() { Intent i = new Intent(this, ManageCategories.class); forwardDataEntryFromWidget(i); //we pass the currently selected category in to prevent //it from being deleted, which can theoretically lead //to crash upon saving https://github.com/mtotschnig/MyExpenses/issues/71 i.putExtra(KEY_ROWID, mCatId); startActivityForResult(i, SELECT_CATEGORY_REQUEST); } /** * listens on changes in the date dialog and sets the date on the button */ private DatePickerDialog.OnDateSetListener mDateSetListener = new DatePickerDialog.OnDateSetListener() { public void onDateSet(DatePicker view, int year, int monthOfYear, int dayOfMonth) { if (mCalendar.get(Calendar.YEAR) != year || mCalendar.get(Calendar.MONTH) != monthOfYear || mCalendar.get(Calendar.DAY_OF_MONTH) != dayOfMonth) { mCalendar.set(year, monthOfYear, dayOfMonth); setDate(); setDirty(true); } } }; /** * listens on changes in the time dialog and sets the time on the button */ private TimePickerDialog.OnTimeSetListener mTimeSetListener = new TimePickerDialog.OnTimeSetListener() { public void onTimeSet(TimePicker view, int hourOfDay, int minute) { if (mCalendar.get(Calendar.HOUR_OF_DAY) != hourOfDay || mCalendar.get(Calendar.MINUTE) != minute) { mCalendar.set(Calendar.HOUR_OF_DAY, hourOfDay); mCalendar.set(Calendar.MINUTE, minute); setTime(); setDirty(true); } } }; @Override protected Dialog onCreateDialog(int id) { switch (id) { case DATE_DIALOG_ID: boolean brokenSamsungDevice = isBrokenSamsungDevice(); @SuppressLint("InlinedApi") Context context = brokenSamsungDevice ? new ContextThemeWrapper(this, MyApplication.getThemeType() == MyApplication.ThemeType.dark ? android.R.style.Theme_Holo_Dialog : android.R.style.Theme_Holo_Light_Dialog) : this; int year = mCalendar.get(Calendar.YEAR); int month = mCalendar.get(Calendar.MONTH); int day = mCalendar.get(Calendar.DAY_OF_MONTH); DatePickerDialog datePickerDialog = new DatePickerDialog(context, mDateSetListener, year, month, day); if (brokenSamsungDevice) { datePickerDialog.setTitle(""); datePickerDialog.updateDate(year, month, day); } return datePickerDialog; case TIME_DIALOG_ID: return new TimePickerDialog(this, mTimeSetListener, mCalendar.get(Calendar.HOUR_OF_DAY), mCalendar.get(Calendar.MINUTE), android.text.format.DateFormat.is24HourFormat(this)); } return null; } private static boolean isBrokenSamsungDevice() { return (Build.MANUFACTURER.equalsIgnoreCase("samsung") && isBetweenAndroidVersions(Build.VERSION_CODES.LOLLIPOP, Build.VERSION_CODES.LOLLIPOP_MR1)); } private static boolean isBetweenAndroidVersions(int min, int max) { return Build.VERSION.SDK_INT >= min && Build.VERSION.SDK_INT <= max; } /** * populates the input fields with a transaction from the database or a new one */ private void populateFields() { //processing data from user switching operation type Transaction cached = (Transaction) getIntent().getSerializableExtra(KEY_CACHED_DATA); isProcessingLinkedAmountInputs = true; mStatusSpinner.setSelection((cached != null ? cached : mTransaction).crStatus.ordinal(), false); mCommentText.setText((cached != null ? cached : mTransaction).comment); if (mIsMainTransactionOrTemplate) { mPayeeText.setText((cached != null ? cached : mTransaction).payee); } if (mTransaction instanceof Template) { mTitleText.setText(((Template) mTransaction).getTitle()); mPlanToggleButton.setChecked(((Template) mTransaction).isPlanExecutionAutomatic()); } else { mReferenceNumberText.setText((cached != null ? cached : mTransaction).referenceNumber); } fillAmount((cached != null ? cached : mTransaction).getAmount().getAmountMajor()); if (mNewInstance) { if (mTransaction instanceof Template) { mTitleText.requestFocus(); } else if (mIsMainTransactionOrTemplate && PrefKey.AUTO_FILL.getBoolean(false)) { mPayeeText.requestFocus(); } } isProcessingLinkedAmountInputs = false; } protected void fillAmount(BigDecimal amount) { int signum = amount.signum(); switch (signum) { case -1: amount = amount.abs(); break; case 1: mType = INCOME; } if (!mNewInstance) { mAmountText.setAmount(amount); } mAmountText.requestFocus(); mAmountText.selectAll(); } /** * extracts the fields from the transaction date for setting them on the buttons */ private void setDateTime() { setDate(); setTime(); } /** * sets date on date button */ private void setDate() { (mTransaction instanceof Template ? mPlanButton : mDateButton) .setText(mDateFormat.format(mCalendar.getTime())); } /** * sets time on time button */ private void setTime() { mTimeButton.setText(mTimeFormat.format(mCalendar.getTime())); } protected void saveState() { if (syncStateAndValidate(true)) { mIsSaving = true; startDbWriteTask(true); if (getIntent().getBooleanExtra(AbstractWidget.EXTRA_START_FROM_WIDGET, false)) { SharedPreferences.Editor e = MyApplication.getInstance().getSettings().edit(); switch (mOperationType) { case MyExpenses.TYPE_TRANSACTION: e.putLong(PREFKEY_TRANSACTION_LAST_ACCOUNT_FROM_WIDGET, mTransaction.accountId); break; case MyExpenses.TYPE_TRANSFER: e.putLong(PREFKEY_TRANSFER_LAST_ACCOUNT_FROM_WIDGET, mTransaction.accountId); e.putLong(PREFKEY_TRANSFER_LAST_TRANSFER_ACCOUNT_FROM_WIDGET, mTransaction.transfer_account); break; case MyExpenses.TYPE_SPLIT: e.putLong(PREFKEY_SPLIT_LAST_ACCOUNT_FROM_WIDGET, mTransaction.accountId); } SharedPreferencesCompat.apply(e); } } else { //prevent this flag from being sticky if form was not valid mCreateNew = false; } } /** * sets the state of the UI on mTransaction * * @return false if any data is not valid, also informs user through toast */ protected boolean syncStateAndValidate(boolean forSave) { boolean validP = true; String title; Account account = getCurrentAccount(); if (account == null) return false; BigDecimal amount = validateAmountInput(forSave); if (amount == null) { //Toast is shown in validateAmountInput validP = false; } else { if (mType == EXPENSE) { amount = amount.negate(); } mTransaction.getAmount().setCurrency(account.currency); mTransaction.getAmount().setAmountMajor(amount);//TODO refactor to better respect encapsulation } mTransaction.accountId = account.getId(); mTransaction.comment = mCommentText.getText().toString(); if (!(mTransaction instanceof SplitPartCategory || mTransaction instanceof SplitPartTransfer)) { mTransaction.setDate(mCalendar.getTime()); } if (mOperationType == MyExpenses.TYPE_TRANSACTION) { mTransaction.setCatId(mCatId); } if (mIsMainTransactionOrTemplate) { mTransaction.setPayee(mPayeeText.getText().toString()); long selected = mMethodSpinner.getSelectedItemId(); mTransaction.methodId = (selected != AdapterView.INVALID_ROW_ID && selected > 0) ? selected : null; } if (mOperationType == MyExpenses.TYPE_TRANSFER) { mTransaction.transfer_account = mTransferAccountSpinner.getSelectedItemId(); final Account transferAccount = Account.getInstanceFromDb(mTransferAccountSpinner.getSelectedItemId()); boolean isSame = account.currency.equals(transferAccount.currency); if (mTransaction instanceof Template) { if (!isSame && amount == null) { BigDecimal transferAmount = validateAmountInput(mTransferAmountText, forSave); if (transferAmount != null) { mTransaction.accountId = transferAccount.getId(); mTransaction.transfer_account = account.getId(); if (mType == INCOME) { transferAmount = transferAmount.negate(); } mTransaction.setAmount(new Money(transferAccount.currency, transferAmount)); mAmountText.setError(null); validP = true; //we only need either amount or transfer amount } } } else { mTransaction.getTransferAmount().setCurrency(transferAccount.currency); if (isSame) { if (amount != null) mTransaction.getTransferAmount().setAmountMajor(amount.negate()); } else { BigDecimal transferAmount = validateAmountInput(mTransferAmountText, forSave); if (transferAmount == null) { //Toast is shown in validateAmountInput validP = false; } else { if (mType == INCOME) { transferAmount = transferAmount.negate(); } mTransaction.getTransferAmount().setAmountMajor(transferAmount); } } } } if (mTransaction instanceof Template) { title = mTitleText.getText().toString(); if (title.equals("")) { mTitleText.setError(getString(R.string.no_title_given)); validP = false; } ((Template) mTransaction).setTitle(title); if (mPlan == null) { if (mReccurenceSpinner.getSelectedItemPosition() > 0) { String description = ((Template) mTransaction).compileDescription(ExpenseEdit.this); mPlan = new Plan(mCalendar, ((Plan.Recurrence) mReccurenceSpinner.getSelectedItem()).toRrule(mCalendar), ((Template) mTransaction).getTitle(), description); ((Template) mTransaction).setPlan(mPlan); } } else { mPlan.description = ((Template) mTransaction).compileDescription(ExpenseEdit.this); mPlan.title = title; ((Template) mTransaction).setPlan(mPlan); } } else { mTransaction.referenceNumber = mReferenceNumberText.getText().toString(); if (forSave && !(mTransaction instanceof SplitPartCategory || mTransaction instanceof SplitPartTransfer)) { if (mReccurenceSpinner.getSelectedItemPosition() > 0) { title = TextUtils.isEmpty(mTransaction.payee) ? (TextUtils.isEmpty(mLabel) ? (TextUtils.isEmpty(mTransaction.comment) ? getString(R.string.menu_create_template) : mTransaction.comment) : mLabel) : mTransaction.payee; mTransaction.originTemplate = new Template(mTransaction, title); mTransaction.originTemplate.setPlanExecutionAutomatic(true); String description = mTransaction.originTemplate.compileDescription(ExpenseEdit.this); mTransaction.originTemplate.setPlan(new Plan(mCalendar, ((Plan.Recurrence) mReccurenceSpinner.getSelectedItem()).toRrule(mCalendar), mTransaction.originTemplate.getTitle(), description)); } } } mTransaction.crStatus = (Transaction.CrStatus) mStatusSpinner.getSelectedItem(); mTransaction.setPictureUri(mPictureUri); return validP; } /* (non-Javadoc) * @see android.app.Activity#onActivityResult(int, int, android.content.Intent) */ @Override protected void onActivityResult(int requestCode, int resultCode, Intent intent) { super.onActivityResult(requestCode, resultCode, intent); if (requestCode == SELECT_CATEGORY_REQUEST && intent != null) { mCatId = intent.getLongExtra("cat_id", 0); mLabel = intent.getStringExtra("label"); mCategoryButton.setText(mLabel); setDirty(true); } if (requestCode == PICTURE_REQUEST_CODE && resultCode == RESULT_OK) { Uri uri; String errorMsg; if (intent == null) { uri = mPictureUriTemp; Log.d(MyApplication.TAG, "got result for PICTURE request, intent null, relying on stored output uri :" + mPictureUriTemp); } else if (intent.getData() != null) { uri = intent.getData(); Log.d(MyApplication.TAG, "got result for PICTURE request, found uri in intent data :" + uri.toString()); } else { Log.d(MyApplication.TAG, "got result for PICTURE request, intent != null, getData() null, relying on stored output uri :" + mPictureUriTemp); uri = mPictureUriTemp; } if (uri != null) { if (isFileAndNotExists(uri)) { errorMsg = "Error while retrieving image: File not found: " + uri; } else { mPictureUri = uri; setPicture(); setDirty(true); return; } } else { errorMsg = "Error while retrieving image: No data found."; } AcraHelper.report(new Exception(errorMsg)); Toast.makeText(this, errorMsg, Toast.LENGTH_LONG).show(); } } protected void setPicture() { if (mPictureUri != null) { mPictureViewContainer.setVisibility(View.VISIBLE); Picasso.with(this).load(mPictureUri).fit() .into((ImageView) mPictureViewContainer.findViewById(R.id.picture)); mAttachPictureButton.setVisibility(View.GONE); } } @Override public void onBackPressed() { cleanup(); super.onBackPressed(); } protected void cleanup() { if (mTransaction instanceof SplitTransaction) { ((SplitTransaction) mTransaction).cleanupCanceledEdit(); } } /** * updates interface based on type (EXPENSE or INCOME) */ protected void configureType() { super.configureType(); if (mPayeeLabel != null) { mPayeeLabel.setText(mType ? R.string.payer : R.string.payee); } if (mTransaction instanceof SplitTransaction) { findSplitPartList().updateBalance(); } setCategoryButton(); } private void configurePlan() { if (mPlan != null) { mPlanButton.setText(Plan.prettyTimeInfo(this, mPlan.rrule, mPlan.dtstart)); if (mTitleText.getText().toString().equals("")) mTitleText.setText(mPlan.title); mPlanToggleButton.setVisibility(View.VISIBLE); mReccurenceSpinner.getSpinner().setVisibility(View.GONE); mPlanButton.setVisibility(View.VISIBLE); pObserver = new PlanObserver(); getContentResolver().registerContentObserver( ContentUris.withAppendedId(Events.CONTENT_URI, mPlan.getId()), false, pObserver); } } private class PlanObserver extends ContentObserver { public PlanObserver() { super(new Handler()); } @Override public void onChange(boolean selfChange) { if (mIsResumed) { refreshPlanData(); } else { mPlanUpdateNeeded = true; } } } @Override protected void onPostResume() { super.onPostResume(); if (mPlanUpdateNeeded) { refreshPlanData(); mPlanUpdateNeeded = false; } } private void refreshPlanData() { if (mPlan != null) { startTaskExecution(TaskExecutionFragment.TASK_INSTANTIATE_PLAN, new Long[] { mPlan.getId() }, null, 0); } else { //seen in report 96a04ce6a647555356751634fee9fc73, need to investigate how this can happen AcraHelper.report(new Exception("Received onChange on ContentOberver for plan, but mPlan is null")); } } private void configureStatusSpinner() { Account a = getCurrentAccount(); mStatusSpinner.getSpinner() .setVisibility((mTransaction instanceof Template || mTransaction instanceof SplitPartCategory || mTransaction instanceof SplitPartTransfer || a == null || a.type.equals(AccountType.CASH)) ? View.GONE : View.VISIBLE); } /** * set label on category button */ private void setCategoryButton() { if (mLabel != null && mLabel.length() != 0) { mCategoryButton.setText(mLabel); } } @Override protected void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); outState.putSerializable(KEY_CALENDAR, mCalendar); //restored in onCreate if (mRowId != 0) { outState.putLong(KEY_ROWID, mRowId); } if (mCatId != null) { outState.putLong(KEY_CATID, mCatId); } outState.putString(KEY_LABEL, mLabel); if (mPictureUri != null) { outState.putParcelable(KEY_PICTURE_URI, mPictureUri); } if (mPictureUriTemp != null) { outState.putParcelable(KEY_PICTURE_URI_TMP, mPictureUriTemp); } long methodId = mMethodSpinner.getSelectedItemId(); if (methodId == android.widget.AdapterView.INVALID_ROW_ID && mMethodId != null) { methodId = mMethodId; } if (methodId != android.widget.AdapterView.INVALID_ROW_ID) { outState.putLong(KEY_METHODID, methodId); } long accountId = mAccountSpinner.getSelectedItemId(); if (accountId == android.widget.AdapterView.INVALID_ROW_ID && mAccountId != null) { accountId = mAccountId; } if (accountId != android.widget.AdapterView.INVALID_ROW_ID) { outState.putLong(KEY_ACCOUNTID, accountId); } if (mOperationType == MyExpenses.TYPE_TRANSFER) { outState.putLong(KEY_TRANSFER_ACCOUNT, mTransferAccountSpinner.getSelectedItemId()); } } private void switchAccountViews() { Spinner accountSpinner = mAccountSpinner.getSpinner(); Spinner transferAccountSpinner = mTransferAccountSpinner.getSpinner(); ViewGroup accountParent = (ViewGroup) findViewById(R.id.AccountRow); ViewGroup transferAccountRow = (ViewGroup) findViewById(R.id.TransferAccountRow); TableLayout table = (TableLayout) findViewById(R.id.Table); View amountRow = table.findViewById(R.id.AmountRow); View transferAmountRow = table.findViewById(R.id.TransferAmountRow); table.removeView(amountRow); table.removeView(transferAmountRow); if (mType == INCOME) { accountParent.removeView(accountSpinner); transferAccountRow.removeView(transferAccountSpinner); accountParent.addView(transferAccountSpinner); transferAccountRow.addView(accountSpinner); table.addView(transferAmountRow, 2); table.addView(amountRow, 4); } else { accountParent.removeView(transferAccountSpinner); transferAccountRow.removeView(accountSpinner); accountParent.addView(accountSpinner); transferAccountRow.addView(transferAccountSpinner); table.addView(amountRow, 2); table.addView(transferAmountRow, 4); } linkAccountLabels(); } public Money getAmount() { Account a = getCurrentAccount(); if (a == null) return null; Money result = new Money(a.currency, 0L); BigDecimal amount = validateAmountInput(false); if (amount == null) { return result; } if (mType == EXPENSE) { amount = amount.negate(); } result.setAmountMajor(amount); return result; } /* * callback of TaskExecutionFragment */ @Override public void onPostExecute(int taskId, Object o) { super.onPostExecute(taskId, o); boolean success; switch (taskId) { case TaskExecutionFragment.TASK_INSTANTIATE_TRANSACTION_2: if (o != null) { Transaction t = (Transaction) o; if (mCatId == null) { mCatId = t.getCatId(); mLabel = t.label; setCategoryButton(); } if (TextUtils.isEmpty(mCommentText.getText().toString())) { mCommentText.setText(t.comment); } if (TextUtils.isEmpty(mAmountText.getText().toString())) { fillAmount(t.getAmount().getAmountMajor()); configureType(); } } break; case TaskExecutionFragment.TASK_INSTANTIATE_TRANSACTION_FROM_TEMPLATE: if (o == null) { Toast.makeText(this, R.string.save_transaction_template_deleted, Toast.LENGTH_LONG).show(); finish(); return; } case TaskExecutionFragment.TASK_INSTANTIATE_TRANSACTION: case TaskExecutionFragment.TASK_INSTANTIATE_TEMPLATE: if (o == null) { Toast.makeText(this, "Object has been deleted from db", Toast.LENGTH_LONG).show(); finish(); return; } mTransaction = (Transaction) o; if (taskId == TaskExecutionFragment.TASK_INSTANTIATE_TRANSACTION_FROM_TEMPLATE) { if (mPlanInstanceId > 0L) { mTransaction.originPlanInstanceId = mPlanInstanceId; } if (mPlanInstanceDate != 0L) { mTransaction.setDate(new Date(mPlanInstanceDate)); } } if (mTransaction instanceof SplitTransaction) { mOperationType = MyExpenses.TYPE_SPLIT; } else if (mTransaction instanceof Template) { mOperationType = ((Template) mTransaction).isTransfer() ? MyExpenses.TYPE_TRANSFER : MyExpenses.TYPE_TRANSACTION; mPlan = ((Template) mTransaction).getPlan(); } else { mOperationType = mTransaction instanceof Transfer ? MyExpenses.TYPE_TRANSFER : MyExpenses.TYPE_TRANSACTION; } if (mPictureUri == null) { // we might have received a picture in onActivityResult before // arriving here, in this case it takes precedence mPictureUri = mTransaction.getPictureUri(); if (mPictureUri != null) { boolean doShowPicture = true; if (isFileAndNotExists(mTransaction.getPictureUri())) { Toast.makeText(this, R.string.image_deleted, Toast.LENGTH_SHORT).show(); doShowPicture = false; } if (doShowPicture) { setPicture(); } } } //if catId has already been set by onRestoreInstanceState, the value might have been edited by the user and has precedence if (mCatId == null) { mCatId = mTransaction.getCatId(); mLabel = mTransaction.label; } if (getIntent().getBooleanExtra(KEY_CLONE, false)) { if (mTransaction instanceof SplitTransaction) { mRowId = mTransaction.getId(); } else { mTransaction.setId(0L); mRowId = 0L; } mTransaction.crStatus = CrStatus.UNRECONCILED; mTransaction.status = STATUS_NONE; mTransaction.setDate(new Date()); mClone = true; } mCalendar.setTime(mTransaction.getDate()); setup(); supportInvalidateOptionsMenu(); break; case TaskExecutionFragment.TASK_MOVE_UNCOMMITED_SPLIT_PARTS: success = (Boolean) o; Account account = mAccounts[mAccountSpinner.getSelectedItemPosition()]; if (success) { updateAccount(account); } else { for (int i = 0; i < mAccounts.length; i++) { if (mAccounts[i].getId() == mTransaction.accountId) { mAccountSpinner.setSelection(i); break; } } Toast.makeText(this, getString(R.string.warning_cannot_move_split_transaction, account.label), Toast.LENGTH_LONG).show(); } break; case TaskExecutionFragment.TASK_INSTANTIATE_PLAN: mPlan = ((Plan) o); configurePlan(); break; } } private boolean isFileAndNotExists(Uri uri) { if (uri.getScheme().equals("file") && !new File(uri.getPath()).exists()) { return true; } return false; } public Account getCurrentAccount() { if (mAccounts == null) { return null; } int selected = mAccountSpinner.getSelectedItemPosition(); if (selected == android.widget.AdapterView.INVALID_POSITION || selected >= mAccounts.length) { return null; } return mAccounts[selected]; } @Override public void onItemSelected(AdapterView<?> parent, View view, int position, long id) { if (parent.getId() != R.id.OperationType) { setDirty(true); } switch (parent.getId()) { case R.id.Recurrence: int visibility = View.GONE; if (id > 0) { if (ContextCompat.checkSelfPermission(ExpenseEdit.this, Manifest.permission.WRITE_CALENDAR) == PackageManager.PERMISSION_GRANTED) { if (PrefKey.NEW_PLAN_ENABLED.getBoolean(true)) { visibility = View.VISIBLE; } else { mReccurenceSpinner.setSelection(0); CommonCommands.showContribDialog(this, ContribFeature.PLANS_UNLIMITED, null); } } else { ActivityCompat.requestPermissions(ExpenseEdit.this, new String[] { Manifest.permission.WRITE_CALENDAR }, ProtectionDelegate.PERMISSIONS_REQUEST_WRITE_CALENDAR); } } if (mTransaction instanceof Template) { mPlanButton.setVisibility(visibility); mPlanToggleButton.setVisibility(visibility); } break; case R.id.Method: if (id > 0) { //ignore first row "no method" merged in mMethodsCursor.moveToPosition(position - 1); if (!(mTransaction instanceof Template)) mReferenceNumberText.setVisibility( mMethodsCursor.getInt(mMethodsCursor.getColumnIndexOrThrow(KEY_IS_NUMBERED)) > 0 ? View.VISIBLE : View.INVISIBLE); } else { mTransaction.methodId = null; mReferenceNumberText.setVisibility(View.GONE); } break; case R.id.Account: final Account account = mAccounts[position]; if (mTransaction instanceof SplitTransaction && findSplitPartList().getSplitCount() > 0) { //call background task for moving parts to new account startTaskExecution(TaskExecutionFragment.TASK_MOVE_UNCOMMITED_SPLIT_PARTS, new Long[] { mTransaction.getId() }, account.getId(), R.string.progress_dialog_updating_split_parts); } else { updateAccount(account); } break; case R.id.OperationType: int newType = ((Integer) mOperationTypeSpinner.getItemAtPosition(position)); if (newType != mOperationType && isValidType(newType)) { if (newType == MyExpenses.TYPE_TRANSFER && !checkTransferEnabled(getCurrentAccount())) { //reset to previous resetOperationType(); } else if (newType == MyExpenses.TYPE_SPLIT) { resetOperationType(); contribFeatureRequested(ContribFeature.SPLIT_TRANSACTION, null); } else { restartWithType(newType); } } break; case R.id.TransferAccount: mTransaction.transfer_account = mTransferAccountSpinner.getSelectedItemId(); configureTransferInput(); break; } } private boolean isValidType(int type) { return type == MyExpenses.TYPE_SPLIT || type == MyExpenses.TYPE_TRANSACTION || type == MyExpenses.TYPE_TRANSFER; } private void updateAccount(Account account) { mTransaction.accountId = account.getId(); setAccountLabel(account); if (mOperationType == MyExpenses.TYPE_TRANSFER) { mTransferAccountSpinner.setSelection(setTransferAccountFilterMap()); mTransaction.transfer_account = mTransferAccountSpinner.getSelectedItemId(); configureTransferInput(); } else { if (!(mTransaction instanceof SplitPartCategory)) { if (mManager.getLoader(METHODS_CURSOR) != null && !mManager.getLoader(METHODS_CURSOR).isReset()) { mManager.restartLoader(METHODS_CURSOR, null, this); } else { mManager.initLoader(METHODS_CURSOR, null, this); } } if (mTransaction instanceof SplitTransaction) { final SplitPartList splitPartList = findSplitPartList(); splitPartList.updateAccount(account); } } configureStatusSpinner(); mAmountText.setFractionDigits(Money.getFractionDigits(account.currency)); //once user has selected account, we no longer want //the passed in KEY_CURRENCY to override it in onLoadFinished getIntent().removeExtra(KEY_CURRENCY); } private void configureTransferInput() { final Account transferAccount = Account.getInstanceFromDb(mTransferAccountSpinner.getSelectedItemId()); final Currency currency = getCurrentAccount().currency; final boolean isSame = currency.equals(transferAccount.currency); findViewById(R.id.TransferAmountRow).setVisibility(isSame ? View.GONE : View.VISIBLE); findViewById(R.id.ExchangeRateRow) .setVisibility(isSame || (mTransaction instanceof Template) ? View.GONE : View.VISIBLE); final String symbol2 = transferAccount.currency.getSymbol(); //noinspection SetTextI18n addCurrencyToLabel((TextView) findViewById(R.id.TransferAmountLabel), symbol2); mTransferAmountText.setFractionDigits(Money.getFractionDigits(transferAccount.currency)); final String symbol1 = currency.getSymbol(); ((TextView) findViewById(R.id.ExchangeRateLabel_1_1)).setText(String.format("1 %s =", symbol1)); ((TextView) findViewById(R.id.ExchangeRateLabel_1_2)).setText(symbol2); ((TextView) findViewById(R.id.ExchangeRateLabel_2_1)).setText(String.format("1 %s =", symbol2)); ((TextView) findViewById(R.id.ExchangeRateLabel_2_2)).setText(symbol1); Bundle bundle = new Bundle(2); bundle.putStringArray(KEY_CURRENCY, new String[] { currency.getCurrencyCode(), transferAccount.currency.getCurrencyCode() }); if (!isSame && !mSavedInstance && (mNewInstance || mPlanInstanceId == -1) && !(mTransaction instanceof Template)) { mManager.restartLoader(LAST_EXCHANGE_CURSOR, bundle, this); } } private void setAccountLabel(Account account) { addCurrencyToLabel(mAmountLabel, account.currency.getSymbol()); } private void addCurrencyToLabel(TextView label, String symbol) { //noinspection SetTextI18n label.setText(getString(R.string.amount) + " (" + symbol + ")"); } private void resetOperationType() { mOperationTypeSpinner.setSelection(mOperationTypeAdapter.getPosition(mOperationType)); } private void restartWithType(int newType) { cleanup(); Intent restartIntent = getIntent(); restartIntent.putExtra(MyApplication.KEY_OPERATION_TYPE, newType); syncStateAndValidate(false); restartIntent.putExtra(KEY_CACHED_DATA, mTransaction); if (mOperationType != MyExpenses.TYPE_SPLIT && newType != MyExpenses.TYPE_SPLIT) { restartIntent.putExtra(KEY_CACHED_RECURRENCE, ((Plan.Recurrence) mReccurenceSpinner.getSelectedItem())); } if (mTransaction.getPictureUri() != null) { restartIntent.putExtra(KEY_CACHED_PICTURE_URI, mTransaction.getPictureUri()); } finish(); startActivity(restartIntent); } @Override public void onNothingSelected(AdapterView<?> parent) { } /* * callback of DbWriteFragment */ @Override public void onPostExecute(Object result) { if (result == null) { Toast.makeText(this, "Unknown error while saving transaction", Toast.LENGTH_SHORT).show(); return; } Long sequenceCount = (Long) result; if (sequenceCount < 0L) { String errorMsg; switch (sequenceCount.intValue()) { case DbWriteFragment.ERROR_EXTERNAL_STORAGE_NOT_AVAILABLE: errorMsg = getString(R.string.external_storage_unavailable); break; case DbWriteFragment.ERROR_PICTURE_SAVE_UNKNOWN: errorMsg = "Error while saving picture"; break; case DbWriteFragment.ERROR_CALENDAR_INTEGRATION_NOT_AVAILABLE: mReccurenceSpinner.setSelection(0); mTransaction.originTemplate = null; errorMsg = "Recurring transactions are not available, because calendar integration is not functional on this device."; break; default: //possibly the selected category has been deleted mCatId = null; mCategoryButton.setText(R.string.select); errorMsg = "Error while saving transaction"; } Toast.makeText(this, errorMsg, Toast.LENGTH_LONG).show(); mCreateNew = false; } else { if (mRecordTemplateWidget) { recordUsage(ContribFeature.TEMPLATE_WIDGET); TemplateWidget.showContribMessage(this); } if (mCreateNew) { mCreateNew = false; if (mOperationType == MyExpenses.TYPE_SPLIT) { mTransaction = SplitTransaction.getNewInstance(mTransaction.accountId); mRowId = mTransaction.getId(); findSplitPartList().updateParent(mRowId); } else { mTransaction.setId(0L); mRowId = 0L; mReccurenceSpinner.getSpinner().setVisibility(View.VISIBLE); mReccurenceSpinner.setSelection(0); mPlanButton.setVisibility(View.GONE); } //while saving the picture might have been moved from temp to permanent mPictureUri = mTransaction.getPictureUri(); mNewInstance = true; mClone = false; switch (mOperationType) { case MyExpenses.TYPE_TRANSACTION: setTitle(R.string.menu_create_transaction); break; case MyExpenses.TYPE_TRANSFER: setTitle(R.string.menu_create_transfer); break; case MyExpenses.TYPE_SPLIT: setTitle(R.string.menu_create_split); break; } isProcessingLinkedAmountInputs = true; mAmountText.setText(""); mTransferAmountText.setText(""); isProcessingLinkedAmountInputs = false; Toast.makeText(this, getString(R.string.save_transaction_and_new_success), Toast.LENGTH_SHORT) .show(); } else { //make sure soft keyboard is closed InputMethodManager im = (InputMethodManager) this.getApplicationContext() .getSystemService(Context.INPUT_METHOD_SERVICE); im.hideSoftInputFromWindow(getWindow().getDecorView().getWindowToken(), InputMethodManager.HIDE_NOT_ALWAYS); Intent intent = new Intent(); intent.putExtra(ContribInfoDialogFragment.KEY_SEQUENCE_COUNT, sequenceCount); setResult(RESULT_OK, intent); finish(); //no need to call super after finish return; } } super.onPostExecute(result); } @Override public Model getObject() { return mTransaction; } @Override public Loader<Cursor> onCreateLoader(int id, Bundle args) { switch (id) { case METHODS_CURSOR: Account a = getCurrentAccount(); if (a == null) return null; return new CursorLoader(this, TransactionProvider.METHODS_URI.buildUpon() .appendPath(TransactionProvider.URI_SEGMENT_TYPE_FILTER) .appendPath(mType == INCOME ? "1" : "-1").appendPath(a.type.name()).build(), null, null, null, null); case ACCOUNTS_CURSOR: return new CursorLoader(this, TransactionProvider.ACCOUNTS_BASE_URI, null, null, null, null); case LAST_EXCHANGE_CURSOR: String[] currencies = args.getStringArray(KEY_CURRENCY); return new CursorLoader(this, Transaction.CONTENT_URI.buildUpon().appendPath(TransactionProvider.URI_SEGMENT_LAST_EXCHANGE) .appendPath(currencies[0]).appendPath(currencies[1]).build(), null, null, null, null); } return null; } @Override public void onLoadFinished(Loader<Cursor> loader, Cursor data) { if (data == null) { return; } int id = loader.getId(); switch (id) { case METHODS_CURSOR: mMethodsCursor = data; View methodContainer = findViewById(R.id.MethodRow); if (mMethodsAdapter == null || !data.moveToFirst()) { methodContainer.setVisibility(View.GONE); } else { methodContainer.setVisibility(View.VISIBLE); MatrixCursor extras = new MatrixCursor(new String[] { KEY_ROWID, KEY_LABEL, KEY_IS_NUMBERED }); extras.addRow(new String[] { "0", "- - - -", "0" }); mMethodsAdapter.swapCursor(new MergeCursor(new Cursor[] { extras, data })); if (mSavedInstance) { mTransaction.methodId = mMethodId; } if (mTransaction.methodId != null) { while (data.isAfterLast() == false) { if (data.getLong(data.getColumnIndex(KEY_ROWID)) == mTransaction.methodId) { mMethodSpinner.setSelection(data.getPosition() + 1); break; } data.moveToNext(); } } else { mMethodSpinner.setSelection(0); } } break; case ACCOUNTS_CURSOR: mAccountsAdapter.swapCursor(data); mAccounts = new Account[data.getCount()]; if (mSavedInstance) { mTransaction.accountId = mAccountId; mTransaction.transfer_account = mTransferAccountId; } data.moveToFirst(); boolean selectionSet = false; String currencyExtra = getIntent().getStringExtra(KEY_CURRENCY); while (data.isAfterLast() == false) { int position = data.getPosition(); Account a = Account.fromCacheOrFromCursor(data); mAccounts[position] = a; if (!selectionSet && (a.currency.getCurrencyCode().equals(currencyExtra) || (currencyExtra == null && a.getId().equals(mTransaction.accountId)))) { mAccountSpinner.setSelection(position); setAccountLabel(a); selectionSet = true; } data.moveToNext(); } //if the accountId we have been passed does not exist, we select the first entry if (mAccountSpinner.getSelectedItemPosition() == android.widget.AdapterView.INVALID_POSITION) { mAccountSpinner.setSelection(0); mTransaction.accountId = mAccounts[0].getId(); setAccountLabel(mAccounts[0]); } if (mOperationType == MyExpenses.TYPE_TRANSFER) { mTransferAccountCursor = new FilterCursorWrapper(data); int selectedPosition = setTransferAccountFilterMap(); mTransferAccountsAdapter.swapCursor(mTransferAccountCursor); mTransferAccountSpinner.setSelection(selectedPosition); mTransaction.transfer_account = mTransferAccountSpinner.getSelectedItemId(); configureTransferInput(); if (!mNewInstance && !(mTransaction instanceof Template)) { isProcessingLinkedAmountInputs = true; mTransferAmountText.setAmount(mTransaction.getTransferAmount().getAmountMajor().abs()); updateExchangeRates(); isProcessingLinkedAmountInputs = false; } } else { //the methods cursor is based on the current account, //hence it is loaded only after the accounts cursor is loaded if (!(mTransaction instanceof SplitPartCategory)) { mManager.initLoader(METHODS_CURSOR, null, this); } } mTypeButton.setEnabled(true); configureType(); configureStatusSpinner(); if (mIsResumed) setupListeners(); break; case LAST_EXCHANGE_CURSOR: if (data.moveToFirst()) { final Currency currency1 = getCurrentAccount().currency; final Currency currency2 = Account .getInstanceFromDb(mTransferAccountSpinner.getSelectedItemId()).currency; if (currency1.getCurrencyCode().equals(data.getString(0)) && currency2.getCurrencyCode().equals(data.getString(1))) { BigDecimal amount = new Money(currency1, data.getLong(2)).getAmountMajor(); BigDecimal transferAmount = new Money(currency2, data.getLong(3)).getAmountMajor(); BigDecimal exchangeRate = amount.compareTo(nullValue) != 0 ? transferAmount.divide(amount, EXCHANGE_RATE_FRACTION_DIGITS, RoundingMode.DOWN) : nullValue; if (exchangeRate.compareTo(nullValue) != 0) { mExchangeRate1Text.setAmount(exchangeRate); } } } } } private int setTransferAccountFilterMap() { Account fromAccount = mAccounts[mAccountSpinner.getSelectedItemPosition()]; ArrayList<Integer> list = new ArrayList<>(); int position = 0, selectedPosition = 0; for (int i = 0; i < mAccounts.length; i++) { if (fromAccount.getId() != mAccounts[i].getId()) { list.add(i); if (mTransaction.transfer_account != null && mTransaction.transfer_account == mAccounts[i].getId()) { selectedPosition = position; } position++; } } mTransferAccountCursor.setFilterMap(list); mTransferAccountsAdapter.notifyDataSetChanged(); return selectedPosition; } private void launchPlanView() { Intent intent = new Intent(Intent.ACTION_VIEW); intent.setData(ContentUris.withAppendedId(Events.CONTENT_URI, mPlan.getId())); //ACTION_VIEW expects to get a range http://code.google.com/p/android/issues/detail?id=23852 intent.putExtra(CalendarContractCompat.EXTRA_EVENT_BEGIN_TIME, mPlan.dtstart); intent.putExtra(CalendarContractCompat.EXTRA_EVENT_END_TIME, mPlan.dtstart); if (Utils.isIntentAvailable(this, intent)) { startActivity(intent); } else { Toast.makeText(this, R.string.no_calendar_app_installed, Toast.LENGTH_SHORT).show(); } } @Override public void onLoaderReset(Loader<Cursor> loader) { //should not be necessary to empty the autocompletetextview int id = loader.getId(); switch (id) { case METHODS_CURSOR: mMethodsCursor = null; if (mMethodsAdapter != null) { mMethodsAdapter.swapCursor(null); } break; case ACCOUNTS_CURSOR: if (mAccountsAdapter != null) { mAccountsAdapter.swapCursor(null); } break; } } public void onToggleClicked(View view) { ((Template) mTransaction).setPlanExecutionAutomatic(((ToggleButton) view).isChecked()); } @Override public void contribFeatureCalled(ContribFeature feature, Serializable tag) { if (feature == ContribFeature.ATTACH_PICTURE) { startMediaChooserDo(); } else if (feature == ContribFeature.SPLIT_TRANSACTION) { restartWithType(MyExpenses.TYPE_SPLIT); } } @Override public void contribFeatureNotCalled(ContribFeature feature) { if (feature == ContribFeature.SPLIT_TRANSACTION) { resetOperationType(); } } public void disableAccountSpinner() { mAccountSpinner.setEnabled(false); } @Override public void onPositive(Bundle args) { switch (args.getInt(ConfirmationDialogFragment.KEY_COMMAND_POSITIVE)) { case R.id.AUTO_FILL_COMMAND: startAutoFill(args.getLong(KEY_ROWID)); PrefKey.AUTO_FILL.putBoolean(true); break; default: super.onPositive(args); } } private void startAutoFill(long id) { startTaskExecution(TaskExecutionFragment.TASK_INSTANTIATE_TRANSACTION_2, new Long[] { id }, null, R.string.progress_dialog_loading); } @Override public void onDismissOrCancel(Bundle args) { if (args.getInt(ConfirmationDialogFragment.KEY_COMMAND_POSITIVE) == R.id.AUTO_FILL_COMMAND) { PrefKey.AUTO_FILL.putBoolean(false); } else { super.onDismissOrCancel(args); } } @Override protected void onPause() { //try to prevent cursor leak mPayeeAdapter.changeCursor(null); mIsResumed = false; super.onPause(); } protected SplitPartList findSplitPartList() { return (SplitPartList) getSupportFragmentManager().findFragmentByTag(SPLIT_PART_LIST); } @SuppressLint("NewApi") public void showPicturePopupMenu(final View v) { PopupMenu popup = new PopupMenu(this, v); popup.setOnMenuItemClickListener(new PopupMenu.OnMenuItemClickListener() { @Override public boolean onMenuItemClick(MenuItem item) { handlePicturePopupMenuClick(item.getItemId()); return true; } }); popup.inflate(R.menu.picture_popup); popup.show(); } private void handlePicturePopupMenuClick(int command) { switch (command) { case R.id.DELETE_COMMAND: mPictureUri = null; mAttachPictureButton.setVisibility(View.VISIBLE); mPictureViewContainer.setVisibility(View.GONE); break; case R.id.VIEW_COMMAND: startActivity(Transaction.getViewIntent(mPictureUri)); break; case R.id.CHANGE_COMMAND: startMediaChooserDo(); break; } } public void startMediaChooser(View v) { contribFeatureRequested(ContribFeature.ATTACH_PICTURE, null); } public void startMediaChooserDo() { Uri outputMediaUri = getCameraUri(); Intent gallIntent = new Intent(Utils.getContentIntentAction()); gallIntent.setType("image/*"); Intent chooserIntent = Intent.createChooser(gallIntent, null); //if external storage is not available, camera capture won't work if (outputMediaUri != null) { Intent camIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE); camIntent.putExtra(MediaStore.EXTRA_OUTPUT, outputMediaUri); chooserIntent.putExtra(Intent.EXTRA_INITIAL_INTENTS, new Intent[] { camIntent }); } Log.d(MyApplication.TAG, "starting chooser for PICTURE_REQUEST with EXTRA_OUTPUT = " + outputMediaUri); startActivityForResult(chooserIntent, ProtectedFragmentActivity.PICTURE_REQUEST_CODE); } private Uri getCameraUri() { if (mPictureUriTemp == null) { mPictureUriTemp = Utils.getOutputMediaUri(true); } return mPictureUriTemp; } @Override public void onRequestPermissionsResult(int requestCode, String permissions[], int[] grantResults) { super.onRequestPermissionsResult(requestCode, permissions, grantResults); switch (requestCode) { case ProtectionDelegate.PERMISSIONS_REQUEST_WRITE_CALENDAR: { // If request is cancelled, the result arrays are empty. if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { if (mTransaction instanceof Template) { mPlanButton.setVisibility(View.VISIBLE); mPlanToggleButton.setVisibility(View.VISIBLE); } } else { mReccurenceSpinner.setSelection(0); if (!ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.WRITE_CALENDAR)) { setPlannerRowVisibility(View.GONE); } } } } } private void updateExchangeRates() { BigDecimal amount = validateAmountInput(mAmountText, false); BigDecimal transferAmount = validateAmountInput(mTransferAmountText, false); BigDecimal exchangeRate = (amount != null && transferAmount != null && amount.compareTo(nullValue) != 0) ? transferAmount.divide(amount, EXCHANGE_RATE_FRACTION_DIGITS, RoundingMode.DOWN) : nullValue; BigDecimal inverseExchangeRate = exchangeRate.compareTo(nullValue) != 0 ? new BigDecimal(1).divide(exchangeRate, EXCHANGE_RATE_FRACTION_DIGITS, RoundingMode.DOWN) : nullValue; mExchangeRate1Text.setAmount(exchangeRate); mExchangeRate2Text.setAmount(inverseExchangeRate); } private class MyTextWatcher implements TextWatcher { public void afterTextChanged(Editable s) { } public void beforeTextChanged(CharSequence s, int start, int count, int after) { } public void onTextChanged(CharSequence s, int start, int before, int count) { } } private class LinkedTransferAmountTextWatcher extends MyTextWatcher { boolean isMain; public LinkedTransferAmountTextWatcher(boolean isMain) { this.isMain = isMain; } @Override public void afterTextChanged(Editable s) { if (isProcessingLinkedAmountInputs) return; isProcessingLinkedAmountInputs = true; if (mTransaction instanceof Template) { (isMain ? mTransferAmountText : mAmountText).setText(""); } else { if (mType == (isMain ? EXPENSE : INCOME)) { BigDecimal input = validateAmountInput(isMain ? mAmountText : mTransferAmountText, false); BigDecimal exchangeRate = validateAmountInput(isMain ? mExchangeRate1Text : mExchangeRate2Text, false); BigDecimal result = exchangeRate != null && input != null ? input.multiply(exchangeRate) : new BigDecimal(0); (isMain ? mTransferAmountText : mAmountText).setAmount(result); } else { updateExchangeRates(); } } isProcessingLinkedAmountInputs = false; } } private class LinkedExchangeRateTextWatchter extends MyTextWatcher { boolean isMain; public LinkedExchangeRateTextWatchter(boolean isMain) { this.isMain = isMain; } @Override public void afterTextChanged(Editable s) { if (isProcessingLinkedAmountInputs) return; isProcessingLinkedAmountInputs = true; BigDecimal input = validateAmountInput(isMain ? mAmountText : mTransferAmountText, false); BigDecimal inputRate = validateAmountInput(isMain ? mExchangeRate1Text : mExchangeRate2Text, false); if (inputRate == null) inputRate = nullValue; BigDecimal inverseInputRate = inputRate.compareTo(nullValue) != 0 ? new BigDecimal(1).divide(inputRate, EXCHANGE_RATE_FRACTION_DIGITS, RoundingMode.DOWN) : nullValue; (isMain ? mExchangeRate2Text : mExchangeRate1Text).setAmount(inverseInputRate); (isMain ? mTransferAmountText : mAmountText) .setAmount(input != null ? input.multiply(inputRate) : nullValue); isProcessingLinkedAmountInputs = false; } } @Override public void showCalculator(View view) { if (view.getId() == R.id.CalculatorTransfer) showCalculatorInternal(mTransferAmountText); else super.showCalculator(view); } @Override protected void onRestoreInstanceState(Bundle savedInstanceState) { isProcessingLinkedAmountInputs = true; super.onRestoreInstanceState(savedInstanceState); isProcessingLinkedAmountInputs = false; } }