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.fragment; import static org.totschnig.myexpenses.provider.DatabaseConstants.*; import org.apache.commons.lang3.ArrayUtils; import org.totschnig.myexpenses.MyApplication; import org.totschnig.myexpenses.R; import org.totschnig.myexpenses.activity.ExpenseEdit; import org.totschnig.myexpenses.activity.ManageCategories; import org.totschnig.myexpenses.activity.MyExpenses; import org.totschnig.myexpenses.activity.ProtectedFragmentActivity; import org.totschnig.myexpenses.adapter.TransactionAdapter; import org.totschnig.myexpenses.dialog.AmountFilterDialog; import org.totschnig.myexpenses.dialog.ConfirmationDialogFragment; import org.totschnig.myexpenses.dialog.DateFilterDialog; import org.totschnig.myexpenses.dialog.EditTextDialog; import org.totschnig.myexpenses.dialog.MessageDialogFragment; import org.totschnig.myexpenses.dialog.SelectCrStatusDialogFragment; import org.totschnig.myexpenses.dialog.SelectMethodDialogFragment; import org.totschnig.myexpenses.dialog.SelectPayerDialogFragment; import org.totschnig.myexpenses.dialog.SelectTransferAccountDialogFragment; import org.totschnig.myexpenses.dialog.TransactionDetailFragment; import org.totschnig.myexpenses.model.Account; import org.totschnig.myexpenses.model.AccountType; import org.totschnig.myexpenses.model.Grouping; import org.totschnig.myexpenses.model.ContribFeature; import org.totschnig.myexpenses.model.Transaction; import org.totschnig.myexpenses.model.Transaction.CrStatus; import org.totschnig.myexpenses.preference.PrefKey; import org.totschnig.myexpenses.preference.SharedPreferencesCompat; import org.totschnig.myexpenses.provider.DatabaseConstants; import org.totschnig.myexpenses.provider.DbUtils; import org.totschnig.myexpenses.provider.TransactionProvider; import org.totschnig.myexpenses.provider.filter.*; import org.totschnig.myexpenses.task.TaskExecutionFragment; import org.totschnig.myexpenses.ui.SimpleCursorAdapter; import org.totschnig.myexpenses.util.AcraHelper; import org.totschnig.myexpenses.util.Result; import org.totschnig.myexpenses.util.Utils; import se.emilsjolander.stickylistheaders.StickyListHeadersAdapter; import se.emilsjolander.stickylistheaders.ExpandableStickyListHeadersListView; import se.emilsjolander.stickylistheaders.StickyListHeadersListView; import se.emilsjolander.stickylistheaders.StickyListHeadersListView.OnHeaderClickListener; import android.annotation.SuppressLint; import android.annotation.TargetApi; import android.app.Activity; import android.content.ContentResolver; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; import android.database.ContentObserver; import android.database.Cursor; import android.graphics.Color; import android.graphics.PorterDuff; import android.graphics.drawable.Drawable; import android.net.Uri.Builder; import android.os.Build; import android.os.Bundle; import android.os.Handler; import android.support.v4.app.DialogFragment; import android.support.v4.app.Fragment; import android.support.v4.app.FragmentManager; import android.support.v4.app.FragmentTransaction; import android.support.v4.app.LoaderManager; import android.support.v4.content.CursorLoader; import android.support.v4.content.Loader; import android.text.TextUtils; import android.util.SparseArray; import android.util.SparseBooleanArray; import android.view.ContextMenu; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuItem; import android.view.SubMenu; import android.view.View; import android.view.ViewGroup; import android.view.ContextMenu.ContextMenuInfo; import android.widget.AbsListView; import android.widget.AdapterView; import android.widget.TextView; import android.widget.AdapterView.AdapterContextMenuInfo; import android.widget.AdapterView.OnItemClickListener; import android.widget.Toast; //TODO: consider moving to ListFragment public class TransactionList extends ContextualActionBarFragment implements LoaderManager.LoaderCallbacks<Cursor>, OnHeaderClickListener, SharedPreferences.OnSharedPreferenceChangeListener { protected int getMenuResource() { return R.menu.transactionlist_context; } protected WhereFilter mFilter = WhereFilter.empty(); private static final int TRANSACTION_CURSOR = 0; private static final int SUM_CURSOR = 1; private static final int GROUPING_CURSOR = 2; public static final String KEY_FILTER = "filter"; public static final String CATEGORY_SEPARATOR = " : ", COMMENT_SEPARATOR = " / "; private MyGroupedAdapter mAdapter; private AccountObserver aObserver; Account mAccount; public boolean hasItems, mappedCategories, mappedPayees, mappedMethods, hasTransfers; private Cursor mTransactionsCursor, mGroupingCursor; private ExpandableStickyListHeadersListView mListView; private LoaderManager mManager; private SparseBooleanArray mappedCategoriesPerGroup; /** * needs to be static, because a new instance is created, but loader is reused */ private static boolean scheduledRestart = false; /** * used to restore list selection when drawer is reopened */ private SparseBooleanArray mCheckedListItems; private int columnIndexYear, columnIndexYearOfWeekStart, columnIndexMonth, columnIndexWeek, columnIndexDay, columnIndexLabelSub, columnIndexPayee, columnIndexCrStatus, columnIndexGroupYear, columnIndexGroupMappedCategories, columnIndexGroupSumInterim, columnIndexGroupSumIncome, columnIndexGroupSumExpense, columnIndexGroupSumTransfer, columnIndexYearOfMonthStart, columnIndexLabelMain, columnIndexGroupSecond; boolean indexesCalculated = false, indexesGroupingCalculated = false; //the following values are cached from the account object, so that we can react to changes in the observer private Grouping mGrouping; private AccountType mType; private String mCurrency; private Long mOpeningBalance; public static Fragment newInstance(long accountId) { TransactionList pageFragment = new TransactionList(); Bundle bundle = new Bundle(); bundle.putSerializable(KEY_ACCOUNTID, accountId); pageFragment.setArguments(bundle); return pageFragment; } @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setHasOptionsMenu(true); mappedCategoriesPerGroup = new SparseBooleanArray(); mAccount = Account.getInstanceFromDb(getArguments().getLong(KEY_ACCOUNTID)); if (mAccount == null) { return; } mGrouping = mAccount.grouping; mType = mAccount.type; mCurrency = mAccount.currency.getCurrencyCode(); mOpeningBalance = mAccount.openingBalance.getAmountMinor(); MyApplication.getInstance().getSettings().registerOnSharedPreferenceChangeListener(this); } private void setAdapter() { Context ctx = getActivity(); // Create an array to specify the fields we want to display in the list String[] from = new String[] { KEY_LABEL_MAIN, KEY_DATE, KEY_AMOUNT }; // and an array of the fields we want to bind those fields to int[] to = new int[] { R.id.category, R.id.date, R.id.amount }; mAdapter = new MyGroupedAdapter(ctx, R.layout.expense_row, null, from, to, 0); mListView.setAdapter(mAdapter); } private void setGrouping() { mAdapter.refreshDateFormat(); restartGroupingLoader(); } private void restartGroupingLoader() { mGroupingCursor = null; if (mManager == null) { //can happen after an orientation change in ExportDialogFragment, when resetting multiple accounts mManager = getLoaderManager(); } if (mManager.getLoader(GROUPING_CURSOR) != null && !mManager.getLoader(GROUPING_CURSOR).isReset()) mManager.restartLoader(GROUPING_CURSOR, null, this); else mManager.initLoader(GROUPING_CURSOR, null, this); } @Override public void onDestroy() { super.onDestroy(); MyApplication.getInstance().getSettings().unregisterOnSharedPreferenceChangeListener(this); if (aObserver != null) { try { ContentResolver cr = getActivity().getContentResolver(); cr.unregisterContentObserver(aObserver); } catch (IllegalStateException ise) { // Do Nothing. Observer has already been unregistered. } } } @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { final MyExpenses ctx = (MyExpenses) getActivity(); if (mAccount == null) { TextView tv = new TextView(ctx); //noinspection SetTextI18n tv.setText("Error loading transaction list for account " + getArguments().getLong(KEY_ACCOUNTID)); return tv; } mManager = getLoaderManager(); //setGrouping(); if (savedInstanceState != null) { mFilter = new WhereFilter(savedInstanceState.getSparseParcelableArray(KEY_FILTER)); } else { restoreFilterFromPreferences(); } View v = inflater.inflate(R.layout.expenses_list, container, false); //TODO check if still needed with Appcompat //work around the problem that the view pager does not display its background correctly with Sherlock if (Build.VERSION.SDK_INT < 11) { v.setBackgroundColor(ctx.getResources() .getColor(PrefKey.UI_THEME_KEY.getString("dark").equals("light") ? android.R.color.white : android.R.color.black)); } mListView = (ExpandableStickyListHeadersListView) v.findViewById(R.id.list); setAdapter(); mListView.setOnHeaderClickListener(this); mListView.setDrawingListUnderStickyHeader(false); if (scheduledRestart) { mManager.restartLoader(TRANSACTION_CURSOR, null, this); mManager.restartLoader(GROUPING_CURSOR, null, this); scheduledRestart = false; } else { mManager.initLoader(GROUPING_CURSOR, null, this); mManager.initLoader(TRANSACTION_CURSOR, null, this); } mManager.initLoader(SUM_CURSOR, null, this); mListView.setEmptyView(v.findViewById(R.id.empty)); mListView.setOnItemClickListener(new OnItemClickListener() { @Override public void onItemClick(AdapterView<?> a, View v, int position, long id) { FragmentManager fm = ctx.getSupportFragmentManager(); DialogFragment f = (DialogFragment) fm.findFragmentByTag(TransactionDetailFragment.class.getName()); if (f == null) { FragmentTransaction ft = fm.beginTransaction(); TransactionDetailFragment.newInstance(id).show(ft, TransactionDetailFragment.class.getName()); } } }); aObserver = new AccountObserver(new Handler()); ContentResolver cr = getActivity().getContentResolver(); //when account has changed, we might have //1) to refresh the list (currency has changed), //2) update current balance(opening balance has changed), //3) update the bottombarcolor (color has changed) //4) refetch grouping cursor (grouping has changed) cr.registerContentObserver(TransactionProvider.ACCOUNTS_URI, true, aObserver); registerForContextualActionBar(mListView.getWrappedList()); return v; } @Override public boolean dispatchCommandMultiple(int command, SparseBooleanArray positions, Long[] itemIds) { MyExpenses ctx = (MyExpenses) getActivity(); FragmentManager fm = ctx.getSupportFragmentManager(); switch (command) { case R.id.DELETE_COMMAND: boolean hasReconciled = false, hasNotVoid = false; for (int i = 0; i < positions.size(); i++) { if (positions.valueAt(i)) { mTransactionsCursor.moveToPosition(positions.keyAt(i)); CrStatus status; try { status = CrStatus.valueOf(mTransactionsCursor.getString(columnIndexCrStatus)); } catch (IllegalArgumentException ex) { status = CrStatus.UNRECONCILED; } if (status == CrStatus.RECONCILED) { hasReconciled = true; } if (status != CrStatus.VOID) { hasNotVoid = true; } if (hasNotVoid && hasReconciled) break; } } String message = getResources().getQuantityString(R.plurals.warning_delete_transaction, itemIds.length, itemIds.length); if (hasReconciled) { message += " " + getString(R.string.warning_delete_reconciled); } Bundle b = new Bundle(); b.putInt(ConfirmationDialogFragment.KEY_TITLE, R.string.dialog_title_warning_delete_transaction); b.putString(ConfirmationDialogFragment.KEY_MESSAGE, message); b.putInt(ConfirmationDialogFragment.KEY_COMMAND_POSITIVE, R.id.DELETE_COMMAND_DO); b.putInt(ConfirmationDialogFragment.KEY_COMMAND_NEGATIVE, R.id.CANCEL_CALLBACK_COMMAND); b.putInt(ConfirmationDialogFragment.KEY_POSITIVE_BUTTON_LABEL, R.string.menu_delete); if (hasNotVoid) { b.putInt(ConfirmationDialogFragment.KEY_CHECKBOX_LABEL, R.string.mark_void_instead_of_delete); } b.putLongArray(TaskExecutionFragment.KEY_OBJECT_IDS, ArrayUtils.toPrimitive(itemIds)); ConfirmationDialogFragment.newInstance(b).show(getFragmentManager(), "DELETE_TRANSACTION"); return true; /* case R.id.CLONE_TRANSACTION_COMMAND: ctx.startTaskExecution( TaskExecutionFragment.TASK_CLONE, itemIds, null, 0); break;*/ case R.id.SPLIT_TRANSACTION_COMMAND: ctx.contribFeatureRequested(ContribFeature.SPLIT_TRANSACTION, itemIds); break; case R.id.UNDELETE_COMMAND: ctx.startTaskExecution(TaskExecutionFragment.TASK_UNDELETE_TRANSACTION, itemIds, null, 0); break; //super is handling deactivation of mActionMode } return super.dispatchCommandMultiple(command, positions, itemIds); } @Override public boolean dispatchCommandSingle(int command, ContextMenu.ContextMenuInfo info) { AdapterContextMenuInfo acmi = (AdapterContextMenuInfo) info; MyExpenses ctx = (MyExpenses) getActivity(); switch (command) { case R.id.EDIT_COMMAND: case R.id.CLONE_TRANSACTION_COMMAND: mTransactionsCursor.moveToPosition(acmi.position); if (DbUtils.getLongOrNull(mTransactionsCursor, "transfer_peer_parent") != null) { Toast.makeText(getActivity(), getString(R.string.warning_splitpartcategory_context), Toast.LENGTH_LONG).show(); } else { Intent i = new Intent(ctx, ExpenseEdit.class); i.putExtra(KEY_ROWID, acmi.id); if (command == R.id.CLONE_TRANSACTION_COMMAND) { i.putExtra(ExpenseEdit.KEY_CLONE, true); } ctx.startActivityForResult(i, MyExpenses.EDIT_TRANSACTION_REQUEST); } //super is handling deactivation of mActionMode break; case R.id.CREATE_TEMPLATE_COMMAND: mTransactionsCursor.moveToPosition(acmi.position); String label = mTransactionsCursor.getString(columnIndexPayee); if (TextUtils.isEmpty(label)) label = mTransactionsCursor.getString(columnIndexLabelSub); if (TextUtils.isEmpty(label)) label = mTransactionsCursor.getString(columnIndexLabelMain); Bundle args = new Bundle(); args.putLong(KEY_ROWID, acmi.id); args.putString(EditTextDialog.KEY_DIALOG_TITLE, getString(R.string.dialog_title_template_title)); args.putString(EditTextDialog.KEY_VALUE, label); args.putInt(EditTextDialog.KEY_REQUEST_CODE, ProtectedFragmentActivity.TEMPLATE_TITLE_REQUEST); EditTextDialog.newInstance(args).show(ctx.getSupportFragmentManager(), "TEMPLATE_TITLE"); return true; } return super.dispatchCommandSingle(command, info); } @Override public Loader<Cursor> onCreateLoader(int id, Bundle arg1) { CursorLoader cursorLoader = null; String selection; String[] selectionArgs; if (mAccount.getId() < 0) { selection = KEY_ACCOUNTID + " IN " + "(SELECT " + KEY_ROWID + " from " + TABLE_ACCOUNTS + " WHERE " + KEY_CURRENCY + " = ? AND " + KEY_EXCLUDE_FROM_TOTALS + "=0)"; selectionArgs = new String[] { mAccount.currency.getCurrencyCode() }; } else { selection = KEY_ACCOUNTID + " = ?"; selectionArgs = new String[] { String.valueOf(mAccount.getId()) }; } switch (id) { case TRANSACTION_CURSOR: if (!mFilter.isEmpty()) { String selectionForParents = mFilter.getSelectionForParents(DatabaseConstants.VIEW_EXTENDED); if (!selectionForParents.equals("")) { selection += " AND " + selectionForParents; selectionArgs = Utils.joinArrays(selectionArgs, mFilter.getSelectionArgs(false)); } } cursorLoader = new CursorLoader(getActivity(), Transaction.EXTENDED_URI, null, selection + " AND " + KEY_PARENTID + " is null", selectionArgs, null); break; //TODO: probably we can get rid of SUM_CURSOR, if we also aggregate unmapped transactions case SUM_CURSOR: cursorLoader = new CursorLoader(getActivity(), TransactionProvider.TRANSACTIONS_URI, new String[] { MAPPED_CATEGORIES, MAPPED_METHODS, MAPPED_PAYEES, HAS_TRANSFERS }, selection, selectionArgs, null); break; case GROUPING_CURSOR: selection = null; selectionArgs = null; Builder builder = TransactionProvider.TRANSACTIONS_URI.buildUpon(); if (!mFilter.isEmpty()) { selection = mFilter.getSelectionForParts(DatabaseConstants.VIEW_EXTENDED);//GROUP query uses extended view if (!selection.equals("")) { selectionArgs = mFilter.getSelectionArgs(true); builder.appendQueryParameter(TransactionProvider.QUERY_PARAMETER_IS_FILTERED, "1"); } } builder.appendPath(TransactionProvider.URI_SEGMENT_GROUPS).appendPath(mAccount.grouping.name()); if (mAccount.getId() < 0) { builder.appendQueryParameter(KEY_CURRENCY, mAccount.currency.getCurrencyCode()); } else { builder.appendQueryParameter(KEY_ACCOUNTID, String.valueOf(mAccount.getId())); } cursorLoader = new CursorLoader(getActivity(), builder.build(), null, selection, selectionArgs, null); break; } return cursorLoader; } @Override public void onLoadFinished(Loader<Cursor> arg0, Cursor c) { switch (arg0.getId()) { case TRANSACTION_CURSOR: mTransactionsCursor = c; hasItems = c.getCount() > 0; if (!indexesCalculated) { columnIndexYear = c.getColumnIndex(KEY_YEAR); columnIndexYearOfWeekStart = c.getColumnIndex(KEY_YEAR_OF_WEEK_START); columnIndexYearOfMonthStart = c.getColumnIndex(KEY_YEAR_OF_MONTH_START); columnIndexMonth = c.getColumnIndex(KEY_MONTH); columnIndexWeek = c.getColumnIndex(KEY_WEEK); columnIndexDay = c.getColumnIndex(KEY_DAY); columnIndexLabelSub = c.getColumnIndex(KEY_LABEL_SUB); columnIndexLabelMain = c.getColumnIndex(KEY_LABEL_MAIN); columnIndexPayee = c.getColumnIndex(KEY_PAYEE_NAME); columnIndexCrStatus = c.getColumnIndex(KEY_CR_STATUS); indexesCalculated = true; } mAdapter.swapCursor(c); invalidateCAB(); break; case SUM_CURSOR: c.moveToFirst(); mappedCategories = c.getInt(c.getColumnIndex(KEY_MAPPED_CATEGORIES)) > 0; mappedPayees = c.getInt(c.getColumnIndex(KEY_MAPPED_PAYEES)) > 0; mappedMethods = c.getInt(c.getColumnIndex(KEY_MAPPED_METHODS)) > 0; hasTransfers = c.getInt(c.getColumnIndex(KEY_HAS_TRANSFERS)) > 0; getActivity().supportInvalidateOptionsMenu(); break; case GROUPING_CURSOR: mGroupingCursor = c; //if the transactionscursor has been loaded before the grouping cursor, we need to refresh //in order to have accurate grouping values if (!indexesGroupingCalculated) { columnIndexGroupYear = c.getColumnIndex(KEY_YEAR); columnIndexGroupSecond = c.getColumnIndex(KEY_SECOND_GROUP); columnIndexGroupSumIncome = c.getColumnIndex(KEY_SUM_INCOME); columnIndexGroupSumExpense = c.getColumnIndex(KEY_SUM_EXPENSES); columnIndexGroupSumTransfer = c.getColumnIndex(KEY_SUM_TRANSFERS); columnIndexGroupMappedCategories = c.getColumnIndex(KEY_MAPPED_CATEGORIES); columnIndexGroupSumInterim = c.getColumnIndex(KEY_INTERIM_BALANCE); indexesGroupingCalculated = true; } if (mTransactionsCursor != null) mAdapter.notifyDataSetChanged(); } } @Override public void onLoaderReset(Loader<Cursor> arg0) { switch (arg0.getId()) { case TRANSACTION_CURSOR: mTransactionsCursor = null; ((SimpleCursorAdapter) mAdapter).swapCursor(null); hasItems = false; break; case SUM_CURSOR: mappedCategories = false; mappedPayees = false; mappedMethods = false; break; case GROUPING_CURSOR: mGroupingCursor = null; } } public boolean isFiltered() { return !mFilter.isEmpty(); } @Override public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { if (key.equals(PrefKey.UI_LANGUAGE.getKey()) || key.equals(PrefKey.GROUP_MONTH_STARTS.getKey()) || key.equals(PrefKey.GROUP_WEEK_STARTS.getKey())) { scheduledRestart = true; } } class AccountObserver extends ContentObserver { public AccountObserver(Handler handler) { super(handler); } public void onChange(boolean selfChange) { if (getActivity() == null || getActivity().isFinishing()) { return; } //if grouping has changed if (mAccount.grouping != mGrouping) { mGrouping = mAccount.grouping; if (mAdapter != null) { setGrouping(); //we should not need to notify here, since setGrouping restarts //the loader and in onLoadFinished we notify //mAdapter.notifyDataSetChanged(); } return; } if (mAccount.type != mType || mAccount.currency.getCurrencyCode() != mCurrency) { mListView.setAdapter(mAdapter); mType = mAccount.type; mCurrency = mAccount.currency.getCurrencyCode(); } if (!mAccount.openingBalance.getAmountMinor().equals(mOpeningBalance)) { restartGroupingLoader(); mOpeningBalance = mAccount.openingBalance.getAmountMinor(); } } } public class MyGroupedAdapter extends TransactionAdapter implements StickyListHeadersAdapter { LayoutInflater inflater; public MyGroupedAdapter(Context context, int layout, Cursor c, String[] from, int[] to, int flags) { super(mAccount, context, layout, c, from, to, flags); inflater = LayoutInflater.from(getActivity()); } @SuppressWarnings("incomplete-switch") @Override public View getHeaderView(int position, View convertView, ViewGroup parent) { HeaderViewHolder holder; if (convertView == null) { convertView = inflater.inflate(R.layout.header, parent, false); holder = new HeaderViewHolder(); holder.text = (TextView) convertView.findViewById(R.id.text); holder.sumExpense = (TextView) convertView.findViewById(R.id.sum_expense); holder.sumIncome = (TextView) convertView.findViewById(R.id.sum_income); holder.sumTransfer = (TextView) convertView.findViewById(R.id.sum_transfer); holder.interimBalance = (TextView) convertView.findViewById(R.id.interim_balance); convertView.setTag(holder); } else { holder = (HeaderViewHolder) convertView.getTag(); } holder.interimBalance.setVisibility(mFilter.isEmpty() ? View.VISIBLE : View.GONE); Cursor c = getCursor(); c.moveToPosition(position); int year = c.getInt(getColumnIndexForYear()); int second = -1; if (mGroupingCursor != null && mGroupingCursor.moveToFirst()) { //no grouping, we need the first and only row if (mAccount.grouping.equals(Grouping.NONE)) { fillSums(holder, mGroupingCursor); } else { traverseCursor: while (!mGroupingCursor.isAfterLast()) { if (mGroupingCursor.getInt(columnIndexGroupYear) == year) { switch (mAccount.grouping) { case YEAR: fillSums(holder, mGroupingCursor); break traverseCursor; case DAY: second = c.getInt(columnIndexDay); if (mGroupingCursor.getInt(columnIndexGroupSecond) != second) break; else { fillSums(holder, mGroupingCursor); break traverseCursor; } case MONTH: second = c.getInt(columnIndexMonth); if (mGroupingCursor.getInt(columnIndexGroupSecond) != second) break; else { fillSums(holder, mGroupingCursor); break traverseCursor; } case WEEK: second = c.getInt(columnIndexWeek); if (mGroupingCursor.getInt(columnIndexGroupSecond) != second) break; else { fillSums(holder, mGroupingCursor); break traverseCursor; } } } mGroupingCursor.moveToNext(); } } if (!mGroupingCursor.isAfterLast()) mappedCategoriesPerGroup.put(position, mGroupingCursor.getInt(columnIndexGroupMappedCategories) > 0); } holder.text.setText(mAccount.grouping.getDisplayTitle(getActivity(), year, second, c)); //holder.text.setText(mAccount.grouping.getDisplayTitle(getActivity(), year, second, mAccount.grouping.equals(Grouping.WEEK)?this_year_of_week_start:this_year, this_week,this_day)); return convertView; } @SuppressLint("SetTextI18n") private void fillSums(HeaderViewHolder holder, Cursor mGroupingCursor) { Long sumExpense = DbUtils.getLongOr0L(mGroupingCursor, columnIndexGroupSumExpense); holder.sumExpense.setText("- " + Utils.convAmount(sumExpense, mAccount.currency)); Long sumIncome = DbUtils.getLongOr0L(mGroupingCursor, columnIndexGroupSumIncome); holder.sumIncome.setText("+ " + Utils.convAmount(sumIncome, mAccount.currency)); Long sumTransfer = DbUtils.getLongOr0L(mGroupingCursor, columnIndexGroupSumTransfer); holder.sumTransfer.setText("<-> " + Utils.convAmount(sumTransfer, mAccount.currency)); Long delta = sumIncome - sumExpense + sumTransfer; if (mFilter.isEmpty()) { Long interimBalance = DbUtils.getLongOr0L(mGroupingCursor, columnIndexGroupSumInterim); Long previousBalance = interimBalance - delta; holder.interimBalance.setText(String.format("%s %s %s = %s", Utils.convAmount(previousBalance, mAccount.currency), Long.signum(delta) > -1 ? "+" : "-", Utils.convAmount(Math.abs(delta), mAccount.currency), Utils.convAmount(interimBalance, mAccount.currency))); } } @Override public long getHeaderId(int position) { if (mAccount.grouping.equals(Grouping.NONE)) return 1; Cursor c = getCursor(); c.moveToPosition(position); int year = c.getInt(getColumnIndexForYear()); int month = c.getInt(columnIndexMonth); int week = c.getInt(columnIndexWeek); int day = c.getInt(columnIndexDay); switch (mAccount.grouping) { case DAY: return year * 1000 + day; case WEEK: return year * 1000 + week; case MONTH: return year * 1000 + month; case YEAR: return year * 1000; default: return 0; } } private int getColumnIndexForYear() { switch (mAccount.grouping) { case WEEK: return columnIndexYearOfWeekStart; case MONTH: return columnIndexYearOfMonthStart; default: return columnIndexYear; } } } class HeaderViewHolder { TextView interimBalance; TextView text; TextView sumIncome; TextView sumExpense; TextView sumTransfer; } @Override public void onHeaderClick(StickyListHeadersListView l, View header, int itemPosition, long headerId, boolean currentlySticky) { if (mListView.isHeaderCollapsed(headerId)) { mListView.expand(headerId); } else { mListView.collapse(headerId); } } @Override public boolean onHeaderLongClick(StickyListHeadersListView l, View header, int itemPosition, long headerId, boolean currentlySticky) { MyExpenses ctx = (MyExpenses) getActivity(); if (mappedCategoriesPerGroup.get(itemPosition)) { ctx.contribFeatureRequested(ContribFeature.DISTRIBUTION, headerId); } else { Toast.makeText(ctx, getString(R.string.no_mapped_transactions), Toast.LENGTH_LONG).show(); } return true; } @Override protected void configureMenuLegacy(Menu menu, ContextMenuInfo menuInfo, int listId) { super.configureMenuLegacy(menu, menuInfo, listId); AdapterContextMenuInfo info = (AdapterContextMenuInfo) menuInfo; configureMenuInternal(menu, isSplitAtPosition(info.position), isVoidAtPosition(info.position), 1); } @TargetApi(Build.VERSION_CODES.HONEYCOMB) @Override protected void configureMenu11(Menu menu, int count, AbsListView lv) { super.configureMenu11(menu, count, lv); SparseBooleanArray checkedItemPositions = lv.getCheckedItemPositions(); boolean hasSplit = false, hasNotVoid = false; for (int i = 0; i < checkedItemPositions.size(); i++) { if (checkedItemPositions.valueAt(i) && isSplitAtPosition(checkedItemPositions.keyAt(i))) { hasSplit = true; break; } } for (int i = 0; i < checkedItemPositions.size(); i++) { if (checkedItemPositions.valueAt(i) && isVoidAtPosition(checkedItemPositions.keyAt(i))) { hasNotVoid = true; break; } } configureMenuInternal(menu, hasSplit, hasNotVoid, count); } private boolean isSplitAtPosition(int position) { if (mTransactionsCursor != null) { //templates for splits is not yet implemented if (mTransactionsCursor.moveToPosition(position) && SPLIT_CATID.equals(DbUtils.getLongOrNull(mTransactionsCursor, KEY_CATID))) { return true; } } return false; } private boolean isVoidAtPosition(int position) { if (mTransactionsCursor != null) { if (mTransactionsCursor.moveToPosition(position)) { CrStatus status; try { status = CrStatus.valueOf(mTransactionsCursor.getString(columnIndexCrStatus)); } catch (IllegalArgumentException ex) { status = CrStatus.UNRECONCILED; } if (status.equals(CrStatus.VOID)) { return true; } } } return false; } private void configureMenuInternal(Menu menu, boolean hasSplit, boolean hasVoid, int count) { menu.findItem(R.id.CREATE_TEMPLATE_COMMAND).setVisible(count == 1 && !hasSplit); menu.findItem(R.id.SPLIT_TRANSACTION_COMMAND).setVisible(!hasSplit && !hasVoid); menu.findItem(R.id.UNDELETE_COMMAND).setVisible(hasVoid); menu.findItem(R.id.EDIT_COMMAND).setVisible(count == 1 && !hasVoid); } @SuppressLint("NewApi") public void onDrawerOpened() { if (mActionMode != null) { mCheckedListItems = mListView.getWrappedList().getCheckedItemPositions().clone(); mActionMode.finish(); } } public void onDrawerClosed() { if (mCheckedListItems != null) { for (int i = 0; i < mCheckedListItems.size(); i++) { if (mCheckedListItems.valueAt(i)) { mListView.getWrappedList().setItemChecked(mCheckedListItems.keyAt(i), true); } } } mCheckedListItems = null; } public void addFilterCriteria(Integer id, Criteria c) { mFilter.put(id, c); SharedPreferencesCompat.apply(MyApplication.getInstance().getSettings().edit() .putString(KEY_FILTER + "_" + c.columnName + "_" + mAccount.getId(), c.toStringExtra())); mManager.restartLoader(TRANSACTION_CURSOR, null, this); mManager.restartLoader(GROUPING_CURSOR, null, this); getActivity().supportInvalidateOptionsMenu(); } /** * Removes a given filter * * @param id * @return true if the filter was set and succesfully removed, false otherwise */ public boolean removeFilter(Integer id) { Criteria c = mFilter.get(id); boolean isFiltered = c != null; if (isFiltered) { SharedPreferencesCompat.apply(MyApplication.getInstance().getSettings().edit() .remove(KEY_FILTER + "_" + c.columnName + "_" + mAccount.getId())); mFilter.remove(id); mManager.restartLoader(TRANSACTION_CURSOR, null, this); mManager.restartLoader(GROUPING_CURSOR, null, this); getActivity().supportInvalidateOptionsMenu(); } return isFiltered; } @Override public void onPrepareOptionsMenu(Menu menu) { super.onPrepareOptionsMenu(menu); if (mAccount == null || getActivity() == null) { //mAccount seen in report 3331195c529454ca6b25a4c5d403beda //getActivity seen in report 68a501c984bdfcc95b40050af4f815bf return; } MenuItem searchMenu = menu.findItem(R.id.SEARCH_COMMAND); if (searchMenu != null) { String title; Drawable searchMenuIcon = searchMenu.getIcon(); if (!mFilter.isEmpty()) { if (searchMenuIcon != null) { searchMenuIcon.setColorFilter(Color.GREEN, PorterDuff.Mode.MULTIPLY); } else { AcraHelper.report(new Exception("Search menu icon not found")); } searchMenu.setChecked(true); title = mAccount.label + " ( " + mFilter.prettyPrint() + " )"; } else { if (searchMenuIcon != null) { searchMenuIcon.setColorFilter(null); } else { AcraHelper.report(new Exception("Search menu icon not found")); } searchMenu.setChecked(false); title = mAccount.label; } ((MyExpenses) getActivity()).setTitle(title); SubMenu filterMenu = searchMenu.getSubMenu(); for (int i = 0; i < filterMenu.size(); i++) { MenuItem filterItem = filterMenu.getItem(i); boolean enabled = true; switch (filterItem.getItemId()) { case R.id.FILTER_CATEGORY_COMMAND: enabled = mappedCategories; break; case R.id.FILTER_STATUS_COMMAND: enabled = !mAccount.type.equals(AccountType.CASH); break; case R.id.FILTER_PAYEE_COMMAND: enabled = mappedPayees; break; case R.id.FILTER_METHOD_COMMAND: enabled = mappedMethods; break; case R.id.FILTER_TRANSFER_COMMAND: enabled = hasTransfers; break; } Criteria c = mFilter.get(filterItem.getItemId()); Utils.menuItemSetEnabledAndVisible(filterItem, enabled || c != null); if (c != null) { filterItem.setChecked(true); filterItem.setTitle(c.prettyPrint()); } } } else { AcraHelper.report(new Exception("Search menu not found")); } } @Override public void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); outState.putSparseParcelableArray(KEY_FILTER, mFilter.getCriteria()); } @Override public boolean onOptionsItemSelected(MenuItem item) { int command = item.getItemId(); switch (command) { case R.id.FILTER_CATEGORY_COMMAND: if (!removeFilter(command)) { Intent i = new Intent(getActivity(), ManageCategories.class); i.setAction("myexpenses.intent.select_filter"); startActivityForResult(i, ProtectedFragmentActivity.FILTER_CATEGORY_REQUEST); } return true; case R.id.FILTER_AMOUNT_COMMAND: if (!removeFilter(command)) { AmountFilterDialog.newInstance(mAccount.currency).show(getActivity().getSupportFragmentManager(), "AMOUNT_FILTER"); } return true; case R.id.FILTER_DATE_COMMAND: if (!removeFilter(command)) { DateFilterDialog.newInstance().show(getActivity().getSupportFragmentManager(), "AMOUNT_FILTER"); } return true; case R.id.FILTER_COMMENT_COMMAND: if (!removeFilter(command)) { Bundle args = new Bundle(); args.putInt(EditTextDialog.KEY_REQUEST_CODE, ProtectedFragmentActivity.FILTER_COMMENT_REQUEST); args.putString(EditTextDialog.KEY_DIALOG_TITLE, getString(R.string.search_comment)); EditTextDialog.newInstance(args).show(getActivity().getSupportFragmentManager(), "COMMENT_FILTER"); } return true; case R.id.FILTER_STATUS_COMMAND: if (!removeFilter(command)) { SelectCrStatusDialogFragment.newInstance().show(getActivity().getSupportFragmentManager(), "STATUS_FILTER"); } return true; case R.id.FILTER_PAYEE_COMMAND: if (!removeFilter(command)) { SelectPayerDialogFragment.newInstance(mAccount.getId()) .show(getActivity().getSupportFragmentManager(), "PAYER_FILTER"); } return true; case R.id.FILTER_METHOD_COMMAND: if (!removeFilter(command)) { SelectMethodDialogFragment.newInstance(mAccount.getId()) .show(getActivity().getSupportFragmentManager(), "METHOD_FILTER"); } return true; case R.id.FILTER_TRANSFER_COMMAND: if (!removeFilter(command)) { SelectTransferAccountDialogFragment.newInstance(mAccount.getId()) .show(getActivity().getSupportFragmentManager(), "TRANSFER_FILTER"); } return true; case R.id.PRINT_COMMAND: MyExpenses ctx = (MyExpenses) getActivity(); Result appDirStatus = Utils.checkAppDir(); if (hasItems) { if (appDirStatus.success) { ctx.contribFeatureRequested(ContribFeature.PRINT, null); } else { Toast.makeText(getActivity(), appDirStatus.print(getActivity()), Toast.LENGTH_LONG).show(); } } else { MessageDialogFragment .newInstance(0, R.string.dialog_command_disabled_reset_account, MessageDialogFragment.Button.okButton(), null, null) .show(ctx.getSupportFragmentManager(), "BUTTON_DISABLED_INFO"); } return true; default: return super.onOptionsItemSelected(item); } } public SparseArray<Criteria> getFilterCriteria() { return mFilter.getCriteria(); } private void restoreFilterFromPreferences() { SharedPreferences settings = MyApplication.getInstance().getSettings(); String filter = settings.getString(KEY_FILTER + "_" + KEY_CATID + "_" + mAccount.getId(), null); if (filter != null) { mFilter.put(R.id.FILTER_CATEGORY_COMMAND, CategoryCriteria.fromStringExtra(filter)); } filter = settings.getString(KEY_FILTER + "_" + KEY_AMOUNT + "_" + mAccount.getId(), null); if (filter != null) { mFilter.put(R.id.FILTER_AMOUNT_COMMAND, AmountCriteria.fromStringExtra(filter)); } filter = settings.getString(KEY_FILTER + "_" + KEY_COMMENT + "_" + mAccount.getId(), null); if (filter != null) { mFilter.put(R.id.FILTER_COMMENT_COMMAND, CommentCriteria.fromStringExtra(filter)); } filter = settings.getString(KEY_FILTER + "_" + KEY_CR_STATUS + "_" + mAccount.getId(), null); if (filter != null) { mFilter.put(R.id.FILTER_STATUS_COMMAND, CrStatusCriteria.fromStringExtra(filter)); } filter = settings.getString(KEY_FILTER + "_" + KEY_PAYEEID + "_" + mAccount.getId(), null); if (filter != null) { mFilter.put(R.id.FILTER_PAYEE_COMMAND, PayeeCriteria.fromStringExtra(filter)); } filter = settings.getString(KEY_FILTER + "_" + KEY_METHODID + "_" + mAccount.getId(), null); if (filter != null) { mFilter.put(R.id.FILTER_METHOD_COMMAND, MethodCriteria.fromStringExtra(filter)); } filter = settings.getString(KEY_FILTER + "_" + KEY_DATE + "_" + mAccount.getId(), null); if (filter != null) { mFilter.put(R.id.FILTER_DATE_COMMAND, DateCriteria.fromStringExtra(filter)); } filter = settings.getString(KEY_FILTER + "_" + KEY_TRANSFER_ACCOUNT + "_" + mAccount.getId(), null); if (filter != null) { mFilter.put(R.id.FILTER_TRANSFER_COMMAND, TransferCriteria.fromStringExtra(filter)); } } @Override public void onActivityResult(int requestCode, int resultCode, Intent intent) { if (requestCode == ProtectedFragmentActivity.FILTER_CATEGORY_REQUEST && resultCode != Activity.RESULT_CANCELED) { String label = intent.getStringExtra(KEY_LABEL); if (resultCode == Activity.RESULT_OK) { long catId = intent.getLongExtra(KEY_CATID, 0); addFilterCriteria(R.id.FILTER_CATEGORY_COMMAND, new CategoryCriteria(label, catId)); } if (resultCode == Activity.RESULT_FIRST_USER) { long[] catIds = intent.getLongArrayExtra(KEY_CATID); addFilterCriteria(R.id.FILTER_CATEGORY_COMMAND, new CategoryCriteria(label, catIds)); } } } }