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 android.app.SearchManager; import android.content.Context; import android.content.Intent; import android.content.res.TypedArray; import android.database.Cursor; import android.graphics.Color; import android.net.Uri.Builder; import android.os.Bundle; import android.support.v4.app.LoaderManager; import android.support.v4.content.CursorLoader; import android.support.v4.content.Loader; import android.support.v4.view.MenuItemCompat; import android.support.v7.app.ActionBar; import android.support.v7.widget.SearchView; import android.text.TextUtils; import android.util.Log; import android.util.SparseBooleanArray; import android.util.TypedValue; import android.view.ContextMenu; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; import android.widget.AbsListView; import android.widget.AdapterView; import android.widget.AdapterView.OnItemSelectedListener; import android.widget.ExpandableListView; import android.widget.ExpandableListView.ExpandableListContextMenuInfo; import android.widget.ExpandableListView.OnChildClickListener; import android.widget.ExpandableListView.OnGroupClickListener; import android.widget.ExpandableListView.OnGroupCollapseListener; import android.widget.ExpandableListView.OnGroupExpandListener; import android.widget.ListView; import android.widget.TextView; import android.widget.Toast; import com.github.mikephil.charting.charts.PieChart; import com.github.mikephil.charting.data.Entry; import com.github.mikephil.charting.data.PieData; import com.github.mikephil.charting.data.PieDataSet; import com.github.mikephil.charting.interfaces.OnChartValueSelectedListener; import com.github.mikephil.charting.utils.ColorTemplate; import org.apache.commons.lang3.ArrayUtils; import org.totschnig.myexpenses.MyApplication; import org.totschnig.myexpenses.MyApplication.ThemeType; import org.totschnig.myexpenses.R; import org.totschnig.myexpenses.activity.ManageCategories; import org.totschnig.myexpenses.activity.ManageCategories.HelpVariant; import org.totschnig.myexpenses.activity.ProtectedFragmentActivity; import org.totschnig.myexpenses.dialog.MessageDialogFragment; import org.totschnig.myexpenses.dialog.SelectMainCategoryDialogFragment; import org.totschnig.myexpenses.dialog.TransactionListDialogFragment; import org.totschnig.myexpenses.model.Account; import org.totschnig.myexpenses.model.Grouping; import org.totschnig.myexpenses.model.Money; import org.totschnig.myexpenses.preference.PrefKey; import org.totschnig.myexpenses.provider.DatabaseConstants; import org.totschnig.myexpenses.provider.DbUtils; import org.totschnig.myexpenses.provider.TransactionProvider; import org.totschnig.myexpenses.task.TaskExecutionFragment; import org.totschnig.myexpenses.ui.SimpleCursorTreeAdapter; import org.totschnig.myexpenses.util.Utils; import java.util.ArrayList; import java.util.Arrays; import java.util.Locale; import static org.totschnig.myexpenses.activity.AmountActivity.EXPENSE; import static org.totschnig.myexpenses.provider.DatabaseConstants.DAY; import static org.totschnig.myexpenses.provider.DatabaseConstants.KEY_ACCOUNTID; import static org.totschnig.myexpenses.provider.DatabaseConstants.KEY_AMOUNT; 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_EXCLUDE_FROM_TOTALS; import static org.totschnig.myexpenses.provider.DatabaseConstants.KEY_LABEL; import static org.totschnig.myexpenses.provider.DatabaseConstants.KEY_LABEL_NORMALIZED; import static org.totschnig.myexpenses.provider.DatabaseConstants.KEY_MAX_VALUE; import static org.totschnig.myexpenses.provider.DatabaseConstants.KEY_PARENTID; import static org.totschnig.myexpenses.provider.DatabaseConstants.KEY_ROWID; import static org.totschnig.myexpenses.provider.DatabaseConstants.KEY_SUM; import static org.totschnig.myexpenses.provider.DatabaseConstants.KEY_THIS_DAY; import static org.totschnig.myexpenses.provider.DatabaseConstants.KEY_THIS_MONTH; import static org.totschnig.myexpenses.provider.DatabaseConstants.KEY_THIS_WEEK; import static org.totschnig.myexpenses.provider.DatabaseConstants.KEY_THIS_YEAR; import static org.totschnig.myexpenses.provider.DatabaseConstants.KEY_THIS_YEAR_OF_WEEK_START; import static org.totschnig.myexpenses.provider.DatabaseConstants.KEY_TYPE; import static org.totschnig.myexpenses.provider.DatabaseConstants.TABLE_ACCOUNTS; import static org.totschnig.myexpenses.provider.DatabaseConstants.TABLE_CATEGORIES; import static org.totschnig.myexpenses.provider.DatabaseConstants.TABLE_TEMPLATES; import static org.totschnig.myexpenses.provider.DatabaseConstants.TABLE_TRANSACTIONS; import static org.totschnig.myexpenses.provider.DatabaseConstants.THIS_DAY; import static org.totschnig.myexpenses.provider.DatabaseConstants.THIS_MONTH; import static org.totschnig.myexpenses.provider.DatabaseConstants.THIS_YEAR; import static org.totschnig.myexpenses.provider.DatabaseConstants.VIEW_COMMITTED; import static org.totschnig.myexpenses.provider.DatabaseConstants.WHERE_NOT_VOID; import static org.totschnig.myexpenses.provider.DatabaseConstants.YEAR; import static org.totschnig.myexpenses.provider.DatabaseConstants.getMonth; import static org.totschnig.myexpenses.provider.DatabaseConstants.getThisWeek; import static org.totschnig.myexpenses.provider.DatabaseConstants.getThisYearOfWeekStart; import static org.totschnig.myexpenses.provider.DatabaseConstants.getWeek; import static org.totschnig.myexpenses.provider.DatabaseConstants.getYearOfMonthStart; import static org.totschnig.myexpenses.provider.DatabaseConstants.getYearOfWeekStart; public class CategoryList extends SortableListFragment implements OnChildClickListener, OnGroupClickListener, LoaderManager.LoaderCallbacks<Cursor> { private static final String KEY_CHILD_COUNT = "child_count"; private View mImportButton; protected int getMenuResource() { return R.menu.categorylist_context; } private static final int SUM_CURSOR = -2; private static final int DATEINFO_CURSOR = -3; private MyExpandableListAdapter mAdapter; private ExpandableListView mListView; private LoaderManager mManager; private TextView incomeSumTv, expenseSumTv; private View bottomLine; private PieChart mChart; public Grouping mGrouping; int mGroupingYear; int mGroupingSecond; int thisYear, thisMonth, thisWeek, thisDay, maxValue; private Account mAccount; private Cursor mGroupCursor; protected boolean mType = EXPENSE; private ArrayList<Integer> mMainColors, mSubColors; private int lastExpandedPosition = -1; boolean showChart = false; boolean aggregateTypes; boolean chartDisplaysSubs; String mFilter; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setHasOptionsMenu(true); } @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { aggregateTypes = PrefKey.DISTRIBUTION_AGGREGATE_TYPES.getBoolean(true); final ManageCategories ctx = (ManageCategories) getActivity(); View v; Bundle extras = ctx.getIntent().getExtras(); mManager = getLoaderManager(); if (ctx.helpVariant.equals(ManageCategories.HelpVariant.distribution)) { showChart = PrefKey.DISTRIBUTION_SHOW_CHART.getBoolean(true); mMainColors = new ArrayList<>(); for (int col : ColorTemplate.PASTEL_COLORS) mMainColors.add(col); for (int col : ColorTemplate.JOYFUL_COLORS) mMainColors.add(col); for (int col : ColorTemplate.LIBERTY_COLORS) mMainColors.add(col); for (int col : ColorTemplate.VORDIPLOM_COLORS) mMainColors.add(col); for (int col : ColorTemplate.COLORFUL_COLORS) mMainColors.add(col); mMainColors.add(ColorTemplate.getHoloBlue()); final long id = Utils.getFromExtra(extras, KEY_ACCOUNTID, 0); mAccount = Account.getInstanceFromDb(id); if (mAccount == null) { TextView tv = new TextView(ctx); //noinspection SetTextI18n tv.setText("Error loading distribution for account " + id); return tv; } Bundle b = savedInstanceState != null ? savedInstanceState : extras; mGrouping = (Grouping) b.getSerializable("grouping"); if (mGrouping == null) mGrouping = Grouping.NONE; mGroupingYear = b.getInt("groupingYear"); mGroupingSecond = b.getInt("groupingSecond"); getActivity().supportInvalidateOptionsMenu(); mManager.initLoader(SUM_CURSOR, null, this); mManager.initLoader(DATEINFO_CURSOR, null, this); v = inflater.inflate(R.layout.distribution_list, container, false); mChart = (PieChart) v.findViewById(R.id.chart1); mChart.setVisibility(showChart ? View.VISIBLE : View.GONE); mChart.setDescription(""); //Typeface tf = Typeface.createFromAsset(getActivity().getAssets(), "OpenSans-Regular.ttf"); //mChart.setValueTypeface(tf); //mChart.setCenterTextTypeface(Typeface.createFromAsset(getActivity().getAssets(), "OpenSans-Light.ttf")); //mChart.setUsePercentValues(true); //mChart.setCenterText("Quarterly\nRevenue"); TypedValue typedValue = new TypedValue(); getActivity().getTheme().resolveAttribute(android.R.attr.textAppearanceMedium, typedValue, true); int[] textSizeAttr = new int[] { android.R.attr.textSize }; int indexOfAttrTextSize = 0; TypedArray a = getActivity().obtainStyledAttributes(typedValue.data, textSizeAttr); int textSize = a.getDimensionPixelSize(indexOfAttrTextSize, -1); a.recycle(); mChart.setCenterTextSizePixels(textSize); // radius of the center hole in percent of maximum radius //mChart.setHoleRadius(60f); //mChart.setTransparentCircleRadius(0f); mChart.setDrawSliceText(false); mChart.setDrawHoleEnabled(true); mChart.setDrawCenterText(true); mChart.setRotationEnabled(false); mChart.setOnChartValueSelectedListener(new OnChartValueSelectedListener() { @Override public void onValueSelected(Entry e, int dataSetIndex) { int index = e.getXIndex(); long packedPosition = (lastExpandedPosition == -1) ? ExpandableListView.getPackedPositionForGroup(index) : ExpandableListView.getPackedPositionForChild(lastExpandedPosition, index); int flatPosition = mListView.getFlatListPosition(packedPosition); mListView.setItemChecked(flatPosition, true); mListView.smoothScrollToPosition(flatPosition); setCenterText(index); } @Override public void onNothingSelected() { mListView.setItemChecked(mListView.getCheckedItemPosition(), false); } }); } else { v = inflater.inflate(R.layout.categories_list, container, false); if (savedInstanceState != null) { mFilter = savedInstanceState.getString("filter"); } } incomeSumTv = (TextView) v.findViewById(R.id.sum_income); expenseSumTv = (TextView) v.findViewById(R.id.sum_expense); bottomLine = v.findViewById(R.id.BottomLine); updateColor(); mListView = (ExpandableListView) v.findViewById(R.id.list); final View emptyView = v.findViewById(R.id.empty); mListView.setEmptyView(emptyView); mImportButton = emptyView.findViewById(R.id.importButton); mManager.initLoader(SORTABLE_CURSOR, null, this); String[] from; int[] to; if (mAccount != null) { from = new String[] { KEY_LABEL, KEY_SUM }; to = new int[] { R.id.label, R.id.amount }; } else { from = new String[] { KEY_LABEL }; to = new int[] { R.id.label }; } mAdapter = new MyExpandableListAdapter(ctx, null, R.layout.category_row, R.layout.category_row, from, to, from, to); mListView.setAdapter(mAdapter); if (ctx.helpVariant.equals(ManageCategories.HelpVariant.distribution)) { mListView.setOnGroupExpandListener(new OnGroupExpandListener() { @Override public void onGroupExpand(int groupPosition) { if (showChart) { if (lastExpandedPosition != -1 && groupPosition != lastExpandedPosition) { mListView.collapseGroup(lastExpandedPosition); } } lastExpandedPosition = groupPosition; } }); mListView.setOnGroupCollapseListener(new OnGroupCollapseListener() { @Override public void onGroupCollapse(int groupPosition) { if (showChart) { lastExpandedPosition = -1; setData(mGroupCursor, mMainColors); highlight(groupPosition); long packedPosition = ExpandableListView.getPackedPositionForGroup(groupPosition); int flatPosition = mListView.getFlatListPosition(packedPosition); mListView.setItemChecked(flatPosition, true); } } }); mListView.setOnChildClickListener(new OnChildClickListener() { @Override public boolean onChildClick(ExpandableListView parent, View v, int groupPosition, int childPosition, long id) { if (showChart) { long packedPosition = ExpandableListView.getPackedPositionForChild(groupPosition, childPosition); highlight(childPosition); int flatPosition = mListView.getFlatListPosition(packedPosition); mListView.setItemChecked(flatPosition, true); return true; } return false; } }); //the following is relevant when not in touch mode mListView.setOnItemSelectedListener(new OnItemSelectedListener() { @Override public void onItemSelected(AdapterView<?> parent, View view, int position, long id) { if (showChart) { long pos = mListView.getExpandableListPosition(position); int type = ExpandableListView.getPackedPositionType(pos); int group = ExpandableListView.getPackedPositionGroup(pos), child = ExpandableListView.getPackedPositionChild(pos); int highlightedPos; if (type == ExpandableListView.PACKED_POSITION_TYPE_GROUP) { if (lastExpandedPosition != group) { mListView.collapseGroup(lastExpandedPosition); } highlightedPos = lastExpandedPosition == -1 ? group : -1; } else { highlightedPos = child; } highlight(highlightedPos); } } @Override public void onNothingSelected(AdapterView<?> parent) { } }); mListView.setChoiceMode(ListView.CHOICE_MODE_SINGLE); registerForContextMenu(mListView); } else { registerForContextualActionBar(mListView); } return v; } @Override public boolean dispatchCommandMultiple(int command, SparseBooleanArray positions, Long[] itemIds) { ManageCategories ctx = (ManageCategories) getActivity(); ArrayList<Long> idList; switch (command) { case R.id.DELETE_COMMAND: int mappedTransactionsCount = 0, mappedTemplatesCount = 0, hasChildrenCount = 0; idList = new ArrayList<>(); for (int i = 0; i < positions.size(); i++) { Cursor c; if (positions.valueAt(i)) { boolean deletable = true; int position = positions.keyAt(i); long pos = mListView.getExpandableListPosition(position); int type = ExpandableListView.getPackedPositionType(pos); int group = ExpandableListView.getPackedPositionGroup(pos), child = ExpandableListView.getPackedPositionChild(pos); if (type == ExpandableListView.PACKED_POSITION_TYPE_CHILD) { c = mAdapter.getChild(group, child); c.moveToPosition(child); } else { c = mGroupCursor; c.moveToPosition(group); } long itemId = c.getLong(c.getColumnIndex(KEY_ROWID)); Bundle extras = ctx.getIntent().getExtras(); if ((extras != null && extras.getLong(KEY_ROWID) == itemId) || c.getInt(c.getColumnIndex(DatabaseConstants.KEY_MAPPED_TRANSACTIONS)) > 0) { mappedTransactionsCount++; deletable = false; } else if (c.getInt(c.getColumnIndex(DatabaseConstants.KEY_MAPPED_TEMPLATES)) > 0) { mappedTemplatesCount++; deletable = false; } if (deletable) { if (type == ExpandableListView.PACKED_POSITION_TYPE_GROUP && c.getInt(c.getColumnIndex(KEY_CHILD_COUNT)) > 0) { hasChildrenCount++; } idList.add(itemId); } } } if (!idList.isEmpty()) { Long[] objectIds = idList.toArray(new Long[idList.size()]); if (hasChildrenCount > 0) { MessageDialogFragment .newInstance(R.string.dialog_title_warning_delete_main_category, getResources().getQuantityString(R.plurals.warning_delete_main_category, hasChildrenCount, hasChildrenCount), new MessageDialogFragment.Button(android.R.string.yes, R.id.DELETE_COMMAND_DO, objectIds), null, new MessageDialogFragment.Button(android.R.string.no, R.id.CANCEL_CALLBACK_COMMAND, null)) .show(ctx.getSupportFragmentManager(), "DELETE_CATEGORY"); } else { ctx.dispatchCommand(R.id.DELETE_COMMAND_DO, objectIds); } } if (mappedTransactionsCount > 0 || mappedTemplatesCount > 0) { String message = ""; if (mappedTransactionsCount > 0) message += getResources().getQuantityString(R.plurals.not_deletable_mapped_transactions, mappedTransactionsCount, mappedTransactionsCount); if (mappedTemplatesCount > 0) message += getResources().getQuantityString(R.plurals.not_deletable_mapped_templates, mappedTemplatesCount, mappedTemplatesCount); Toast.makeText(getActivity(), message, Toast.LENGTH_LONG).show(); } return true; case R.id.SELECT_COMMAND_MULTIPLE: ArrayList<String> labelList = new ArrayList<>(); for (int i = 0; i < positions.size(); i++) { Cursor c; if (positions.valueAt(i)) { int position = positions.keyAt(i); long pos = mListView.getExpandableListPosition(position); int type = ExpandableListView.getPackedPositionType(pos); int group = ExpandableListView.getPackedPositionGroup(pos), child = ExpandableListView.getPackedPositionChild(pos); if (type == ExpandableListView.PACKED_POSITION_TYPE_CHILD) { c = mAdapter.getChild(group, child); c.moveToPosition(child); } else { c = mGroupCursor; c.moveToPosition(group); } labelList.add(c.getString(c.getColumnIndex(KEY_LABEL))); } } Intent intent = new Intent(); intent.putExtra(KEY_CATID, ArrayUtils.toPrimitive(itemIds)); intent.putExtra(KEY_LABEL, TextUtils.join(",", labelList)); ctx.setResult(ManageCategories.RESULT_FIRST_USER, intent); ctx.finish(); return true; case R.id.MOVE_COMMAND: final Long[] excludedIds; final boolean inGroup = expandableListSelectionType == ExpandableListView.PACKED_POSITION_TYPE_GROUP; if (inGroup) { excludedIds = itemIds; } else { idList = new ArrayList<>(); for (int i = 0; i < positions.size(); i++) { if (positions.valueAt(i)) { int position = positions.keyAt(i); long pos = mListView.getExpandableListPosition(position); int group = ExpandableListView.getPackedPositionGroup(pos); mGroupCursor.moveToPosition(group); idList.add(mGroupCursor.getLong(mGroupCursor.getColumnIndex(KEY_ROWID))); } } excludedIds = idList.toArray(new Long[idList.size()]); } Bundle args = new Bundle(3); args.putBoolean(SelectMainCategoryDialogFragment.KEY_WITH_ROOT, !inGroup); args.putLongArray(SelectMainCategoryDialogFragment.KEY_EXCLUDED_ID, ArrayUtils.toPrimitive(excludedIds)); args.putLongArray(TaskExecutionFragment.KEY_OBJECT_IDS, ArrayUtils.toPrimitive(itemIds)); SelectMainCategoryDialogFragment.newInstance(args).show(getFragmentManager(), "SELECT_TARGET"); return true; } return false; } @Override public boolean dispatchCommandSingle(int command, ContextMenu.ContextMenuInfo info) { ManageCategories ctx = (ManageCategories) getActivity(); ExpandableListContextMenuInfo elcmi = (ExpandableListContextMenuInfo) info; int type = ExpandableListView.getPackedPositionType(elcmi.packedPosition); Cursor c; boolean isMain; int group = ExpandableListView.getPackedPositionGroup(elcmi.packedPosition), child = ExpandableListView.getPackedPositionChild(elcmi.packedPosition); if (type == ExpandableListView.PACKED_POSITION_TYPE_CHILD) { c = mAdapter.getChild(group, child); isMain = false; } else { c = mGroupCursor; isMain = true; } if (c == null || c.getCount() == 0) { //observed on Blackberry Z10 return false; } String label = c.getString(c.getColumnIndex(KEY_LABEL)); switch (command) { case R.id.EDIT_COMMAND: ctx.editCat(label, elcmi.id); return true; case R.id.SELECT_COMMAND: if (!isMain && ctx.helpVariant.equals(ManageCategories.HelpVariant.select_mapping)) { mGroupCursor.moveToPosition(group); label = mGroupCursor.getString(mGroupCursor.getColumnIndex(KEY_LABEL)) + TransactionList.CATEGORY_SEPARATOR + label; } doSelection(elcmi.id, label, isMain); finishActionMode(); return true; case R.id.CREATE_COMMAND: ctx.createCat(elcmi.id); return true; } return super.dispatchCommandSingle(command, info); } /** * Mapping the categories table into the ExpandableList * * @author Michael Totschnig */ public class MyExpandableListAdapter extends SimpleCursorTreeAdapter { private int colorExpense; private int colorIncome; public MyExpandableListAdapter(Context context, Cursor cursor, int groupLayout, int childLayout, String[] groupFrom, int[] groupTo, String[] childrenFrom, int[] childrenTo) { super(context, cursor, groupLayout, groupFrom, groupTo, childLayout, childrenFrom, childrenTo); colorIncome = ((ProtectedFragmentActivity) context).getColorIncome(); colorExpense = ((ProtectedFragmentActivity) context).getColorExpense(); } /* (non-Javadoc) * returns a cursor with the subcategories for the group * @see android.widget.CursorTreeAdapter#getChildrenCursor(android.database.Cursor) */ @Override protected Cursor getChildrenCursor(Cursor groupCursor) { // Given the group, we return a cursor for all the children within that group long parentId = groupCursor.getLong(groupCursor.getColumnIndexOrThrow(KEY_ROWID)); Bundle bundle = new Bundle(); bundle.putLong("parent_id", parentId); int groupPos = groupCursor.getPosition(); if (mManager.getLoader(groupPos) != null && !mManager.getLoader(groupPos).isReset()) { try { mManager.restartLoader(groupPos, bundle, CategoryList.this); } catch (NullPointerException e) { // a NPE is thrown in the following scenario: //1)open a group //2)orientation change //3)open the same group again //in this scenario getChildrenCursor is called twice, second time leads to error //maybe it is trying to close the group that had been kept open before the orientation change e.printStackTrace(); } } else { mManager.initLoader(groupPos, bundle, CategoryList.this); } return null; } @Override public void setViewText(TextView v, String text) { switch (v.getId()) { case R.id.amount: v.setTextColor(Long.valueOf(text) < 0 ? colorExpense : colorIncome); text = Utils.convAmount(text, mAccount.currency); } super.setViewText(v, text); } @Override public View getGroupView(int groupPosition, boolean isExpanded, View convertView, ViewGroup parent) { convertView = super.getGroupView(groupPosition, isExpanded, convertView, parent); View colorView = convertView.findViewById(R.id.color1); if (showChart) { colorView.setBackgroundColor(mMainColors.get(groupPosition % mMainColors.size())); } else { colorView.setVisibility(View.GONE); } return convertView; } @Override public View getChildView(int groupPosition, int childPosition, boolean isLastChild, View convertView, ViewGroup parent) { convertView = super.getChildView(groupPosition, childPosition, isLastChild, convertView, parent); View colorView = convertView.findViewById(R.id.color1); if (showChart) { colorView.setBackgroundColor(mSubColors.get(childPosition % mSubColors.size())); } else { colorView.setVisibility(View.GONE); } return convertView; } } private String buildGroupingClause() { String year = YEAR + " = " + mGroupingYear; switch (mGrouping) { case YEAR: return year; case DAY: return year + " AND " + DAY + " = " + mGroupingSecond; case WEEK: return getYearOfWeekStart() + " = " + mGroupingYear + " AND " + getWeek() + " = " + mGroupingSecond; case MONTH: return getYearOfMonthStart() + " = " + mGroupingYear + " AND " + getMonth() + " = " + mGroupingSecond; default: return null; } } @Override public Loader<Cursor> onCreateLoader(int id, Bundle bundle) { if (getActivity() == null) return null; if (id == SUM_CURSOR) { Builder builder = TransactionProvider.TRANSACTIONS_SUM_URI.buildUpon(); if (mAccount.getId() < 0) { builder.appendQueryParameter(KEY_CURRENCY, mAccount.currency.getCurrencyCode()); } else { builder.appendQueryParameter(KEY_ACCOUNTID, String.valueOf(mAccount.getId())); } return new CursorLoader(getActivity(), builder.build(), null, buildGroupingClause(), null, null); } if (id == DATEINFO_CURSOR) { ArrayList<String> projectionList = new ArrayList<>( Arrays.asList(new String[] { getThisYearOfWeekStart() + " AS " + KEY_THIS_YEAR_OF_WEEK_START, THIS_YEAR + " AS " + KEY_THIS_YEAR, THIS_MONTH + " AS " + KEY_THIS_MONTH, getThisWeek() + " AS " + KEY_THIS_WEEK, THIS_DAY + " AS " + KEY_THIS_DAY })); //if we are at the beginning of the year we are interested in the max of the previous year int yearToLookUp = mGroupingSecond == 1 ? mGroupingYear - 1 : mGroupingYear; switch (mGrouping) { case DAY: projectionList.add( String.format(Locale.US, "strftime('%%j','%d-12-31') AS " + KEY_MAX_VALUE, yearToLookUp)); break; case WEEK: projectionList.add( String.format(Locale.US, "strftime('%%W','%d-12-31') AS " + KEY_MAX_VALUE, yearToLookUp)); break; case MONTH: projectionList.add("12 as " + KEY_MAX_VALUE); break; default: projectionList.add("0 as " + KEY_MAX_VALUE); } if (mGrouping.equals(Grouping.WEEK)) { //we want to find out the week range when we are given a week number //we find out the first Monday in the year, which is the beginning of week 1 and than //add (weekNumber-1)*7 days to get at the beginning of the week projectionList.add(DbUtils.weekStartFromGroupSqlExpression(mGroupingYear, mGroupingSecond)); projectionList.add(DbUtils.weekEndFromGroupSqlExpression(mGroupingYear, mGroupingSecond)); } return new CursorLoader(getActivity(), TransactionProvider.DUAL_URI, projectionList.toArray(new String[projectionList.size()]), null, null, null); } //SORTABLE_CURSOR long parentId; String selection = "", accountSelector = "", sortOrder = null; String[] selectionArgs, projection; String CATTREE_WHERE_CLAUSE = KEY_CATID + " IN (SELECT " + TABLE_CATEGORIES + "." + KEY_ROWID + " UNION SELECT " + KEY_ROWID + " FROM " + TABLE_CATEGORIES + " subtree WHERE " + KEY_PARENTID + " = " + TABLE_CATEGORIES + "." + KEY_ROWID + ")"; String CHILD_COUNT_SELECT = "(select count(*) FROM " + TABLE_CATEGORIES + " subtree where " + KEY_PARENTID + " = " + TABLE_CATEGORIES + "." + KEY_ROWID + ") as " + KEY_CHILD_COUNT; String catFilter; if (mAccount != null) { //Distribution String accountSelection; if (mAccount.getId() < 0) { accountSelection = " IN " + "(SELECT " + KEY_ROWID + " from " + TABLE_ACCOUNTS + " WHERE " + KEY_CURRENCY + " = ? AND " + KEY_EXCLUDE_FROM_TOTALS + " = 0 )"; accountSelector = mAccount.currency.getCurrencyCode(); } else { accountSelection = " = ?"; accountSelector = String.valueOf(mAccount.getId()); } catFilter = "FROM " + VIEW_COMMITTED + " WHERE " + WHERE_NOT_VOID + " AND " + KEY_ACCOUNTID + accountSelection; if (!aggregateTypes) { catFilter += " AND " + KEY_AMOUNT + (mType == EXPENSE ? "<" : ">") + "0"; } if (!mGrouping.equals(Grouping.NONE)) { catFilter += " AND " + buildGroupingClause(); } //we need to include transactions mapped to children for main categories if (bundle == null) { catFilter += " AND " + CATTREE_WHERE_CLAUSE; } else { catFilter += " AND " + KEY_CATID + " = " + TABLE_CATEGORIES + "." + KEY_ROWID; } selection = " AND exists (SELECT 1 " + catFilter + ")"; projection = new String[] { KEY_ROWID, KEY_LABEL, CHILD_COUNT_SELECT, "(SELECT sum(amount) " + catFilter + ") AS " + KEY_SUM }; sortOrder = "abs(" + KEY_SUM + ") DESC"; } else { //manage, select if (bundle == null) { catFilter = CATTREE_WHERE_CLAUSE; } else { catFilter = KEY_CATID + " = " + TABLE_CATEGORIES + "." + KEY_ROWID; } projection = new String[] { KEY_ROWID, KEY_LABEL, CHILD_COUNT_SELECT, //here we do not filter out void transactinos since they need to be considered as mapped "(select 1 FROM " + TABLE_TRANSACTIONS + " WHERE " + catFilter + ") AS " + DatabaseConstants.KEY_MAPPED_TRANSACTIONS, "(select 1 FROM " + TABLE_TEMPLATES + " WHERE " + catFilter + ") AS " + DatabaseConstants.KEY_MAPPED_TEMPLATES }; } boolean isFiltered = !TextUtils.isEmpty(mFilter); String filterSelection = KEY_LABEL_NORMALIZED + " LIKE ?"; String[] filterSelectArgs = { "%" + mFilter + "%", "%" + mFilter + "%" }; if (bundle == null) { //group cursor selection = KEY_PARENTID + " is null" + selection; if (isFiltered) { selection += " AND (" + filterSelection + " OR EXISTS (SELECT 1 FROM " + TABLE_CATEGORIES + " subtree WHERE " + KEY_PARENTID + " = " + TABLE_CATEGORIES + "." + KEY_ROWID + " AND (" + filterSelection + " )))"; } selectionArgs = mAccount != null ? new String[] { accountSelector, accountSelector } : (isFiltered ? filterSelectArgs : null); } else { //child cursor parentId = bundle.getLong(KEY_PARENTID); selection = KEY_PARENTID + " = ?" + selection; if (isFiltered) { selection += " AND (" + filterSelection + " OR (SELECT " + KEY_LABEL_NORMALIZED + " FROM " + TABLE_CATEGORIES + " parent WHERE " + KEY_ROWID + " = " + TABLE_CATEGORIES + "." + KEY_PARENTID + ") LIKE ?)"; } selectionArgs = mAccount != null ? new String[] { accountSelector, String.valueOf(parentId), accountSelector } : (isFiltered ? Utils.joinArrays(new String[] { String.valueOf(parentId) }, filterSelectArgs) : new String[] { String.valueOf(parentId) }); } return new CursorLoader(getActivity(), TransactionProvider.CATEGORIES_URI, projection, selection, selectionArgs, sortOrder); } @Override public void onLoadFinished(Loader<Cursor> loader, Cursor c) { if (getActivity() == null) return; int id = loader.getId(); ProtectedFragmentActivity ctx = (ProtectedFragmentActivity) getActivity(); ActionBar actionBar = ctx.getSupportActionBar(); switch (id) { case SUM_CURSOR: boolean[] seen = new boolean[2]; c.moveToFirst(); while (c.isAfterLast() == false) { int type = c.getInt(c.getColumnIndex(KEY_TYPE)); updateSum(type > 0 ? "+ " : "- ", type > 0 ? incomeSumTv : expenseSumTv, c.getLong(c.getColumnIndex(KEY_SUM))); c.moveToNext(); seen[type] = true; } //if we have no income or expense, there is no row in the cursor if (!seen[1]) updateSum("+ ", incomeSumTv, 0); if (!seen[0]) updateSum("- ", expenseSumTv, 0); break; case DATEINFO_CURSOR: c.moveToFirst(); actionBar.setSubtitle(mGrouping.getDisplayTitle(ctx, mGroupingYear, mGroupingSecond, c)); thisYear = c.getInt(c.getColumnIndex(KEY_THIS_YEAR)); thisMonth = c.getInt(c.getColumnIndex(KEY_THIS_MONTH)); thisWeek = c.getInt(c.getColumnIndex(KEY_THIS_WEEK)); thisDay = c.getInt(c.getColumnIndex(KEY_THIS_DAY)); maxValue = c.getInt(c.getColumnIndex(KEY_MAX_VALUE)); break; case SORTABLE_CURSOR: mGroupCursor = c; mAdapter.setGroupCursor(c); if (ctx.helpVariant.equals(ManageCategories.HelpVariant.distribution)) { if (c.getCount() > 0) { if (lastExpandedPosition == -1) { mChart.setVisibility(showChart ? View.VISIBLE : View.GONE); setData(c, mMainColors); highlight(0); if (showChart) mListView.setItemChecked( mListView.getFlatListPosition(ExpandableListView.getPackedPositionForGroup(0)), true); } } else { mChart.setVisibility(View.GONE); } } if (mAccount != null) { actionBar.setTitle(mAccount.label); } invalidateCAB(); //only need to do this for group since children's cab does not depnd on cursor break; default: //check if group still exists if (mAdapter.getGroupId(id) != 0) { mAdapter.setChildrenCursor(id, c); if (ctx.helpVariant.equals(ManageCategories.HelpVariant.distribution)) { long packedPosition; if (c.getCount() > 0) { mSubColors = getSubColors(mMainColors.get(id % mMainColors.size())); setData(c, mSubColors); highlight(0); packedPosition = ExpandableListView.getPackedPositionForChild(id, 0); } else { packedPosition = ExpandableListView.getPackedPositionForGroup(id); if (!chartDisplaysSubs) {//check if a loader running concurrently has already switched to subs highlight(id); } } if (showChart && id == lastExpandedPosition) { //if user has expanded a new group before loading is finished, getFlatListPosition //would run in an NPE mListView.setItemChecked(mListView.getFlatListPosition(packedPosition), true); } } } } } @Override public void onLoaderReset(Loader<Cursor> loader) { int id = loader.getId(); if (id == SORTABLE_CURSOR) { mGroupCursor = null; mAdapter.setGroupCursor(null); } else if (id > 0) { // child cursor try { mAdapter.setChildrenCursor(id, null); } catch (NullPointerException e) { Log.w("TAG", "Adapter expired, try again on the next query: " + e.getMessage()); } } } @Override protected PrefKey getSortOrderPrefKey() { return PrefKey.SORT_ORDER_CATEGORIES; } @Override public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { ManageCategories ctx = (ManageCategories) getActivity(); if (ctx == null) return; if (!ctx.helpVariant.equals(ManageCategories.HelpVariant.distribution)) { inflater.inflate(R.menu.search, menu); SearchManager searchManager = (SearchManager) getActivity().getSystemService(Context.SEARCH_SERVICE); MenuItem searchMenuItem = menu.findItem(R.id.search); SearchView searchView = (SearchView) MenuItemCompat.getActionView(searchMenuItem); searchView.setSearchableInfo(searchManager.getSearchableInfo(getActivity().getComponentName())); //searchView.setIconifiedByDefault(true); searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() { @Override public boolean onQueryTextSubmit(String query) { return false; } @Override public boolean onQueryTextChange(String newText) { if (TextUtils.isEmpty(newText)) { mFilter = ""; mImportButton.setVisibility(View.VISIBLE); } else { mFilter = Utils.esacapeSqlLikeExpression(Utils.normalize(newText)); // if a filter results in an empty list, // we do not want to show the setup default categories button mImportButton.setVisibility(View.GONE); } collapseAll(); mManager.restartLoader(SORTABLE_CURSOR, null, CategoryList.this); return true; } }); } } @Override public void onPrepareOptionsMenu(Menu menu) { super.onPrepareOptionsMenu(menu); if (mGrouping != null) { Utils.configureGroupingMenu(menu.findItem(R.id.GROUPING_COMMAND).getSubMenu(), mGrouping); boolean grouped = !mGrouping.equals(Grouping.NONE); Utils.menuItemSetEnabledAndVisible(menu.findItem(R.id.FORWARD_COMMAND), grouped); Utils.menuItemSetEnabledAndVisible(menu.findItem(R.id.BACK_COMMAND), grouped); } MenuItem m = menu.findItem(R.id.TOGGLE_CHART_COMMAND); if (m != null) { m.setChecked(showChart); } m = menu.findItem(R.id.TOGGLE_AGGREGATE_TYPES); if (m != null) { m.setChecked(aggregateTypes); Utils.menuItemSetEnabledAndVisible(menu.findItem(R.id.switchId), !aggregateTypes); } MenuItem searchMenuItem = menu.findItem(R.id.search); if (searchMenuItem != null && mFilter != null) { SearchView searchView = (SearchView) MenuItemCompat.getActionView(searchMenuItem); searchView.setQuery(mFilter, false); searchView.setIconified(false); searchView.clearFocus(); } } public void back() { if (mGrouping.equals(Grouping.YEAR)) mGroupingYear--; else { mGroupingSecond--; if (mGroupingSecond < 1) { mGroupingYear--; mGroupingSecond = maxValue; } } reset(); } public void forward() { if (mGrouping.equals(Grouping.YEAR)) mGroupingYear++; else { mGroupingSecond++; if (mGroupingSecond > maxValue) { mGroupingYear++; mGroupingSecond = 1; } } reset(); } @Override public boolean onOptionsItemSelected(MenuItem item) { if (handleGrouping(item)) return true; switch (item.getItemId()) { case R.id.BACK_COMMAND: back(); return true; case R.id.FORWARD_COMMAND: forward(); return true; case R.id.TOGGLE_CHART_COMMAND: showChart = !showChart; PrefKey.DISTRIBUTION_SHOW_CHART.putBoolean(showChart); mChart.setVisibility(showChart ? View.VISIBLE : View.GONE); if (showChart) { collapseAll(); } else { mListView.setItemChecked(mListView.getCheckedItemPosition(), false); } return true; case R.id.TOGGLE_AGGREGATE_TYPES: aggregateTypes = !aggregateTypes; PrefKey.DISTRIBUTION_AGGREGATE_TYPES.putBoolean(aggregateTypes); getActivity().supportInvalidateOptionsMenu(); reset(); return true; } return handleSortOption(item); } private boolean handleGrouping(MenuItem item) { Grouping newGrouping = Utils.getGroupingFromMenuItemId(item.getItemId()); if (newGrouping != null) { if (!item.isChecked()) { setGrouping(newGrouping); } return true; } return false; } public void collapseAll() { int count = mAdapter.getGroupCount(); for (int i = 0; i < count; i++) mListView.collapseGroup(i); } /* (non-Javadoc) * return the sub cat to the calling activity * @see android.app.ExpandableListActivity#onChildClick(android.widget.ExpandableListView, android.view.View, int, int, long) */ @Override public boolean onChildClick(ExpandableListView parent, View v, int groupPosition, int childPosition, long id) { if (super.onChildClick(parent, v, groupPosition, childPosition, id)) return true; ManageCategories ctx = (ManageCategories) getActivity(); if (ctx == null || ctx.helpVariant.equals(ManageCategories.HelpVariant.manage)) { return false; } String label = ((TextView) v.findViewById(R.id.label)).getText().toString(); if (ctx.helpVariant.equals(ManageCategories.HelpVariant.select_mapping)) { mGroupCursor.moveToPosition(groupPosition); label = mGroupCursor.getString(mGroupCursor.getColumnIndex(KEY_LABEL)) + TransactionList.CATEGORY_SEPARATOR + label; } doSelection(id, label, false); return true; } @Override public boolean onGroupClick(ExpandableListView parent, View v, int groupPosition, long id) { if (super.onGroupClick(parent, v, groupPosition, id)) return true; ManageCategories ctx = (ManageCategories) getActivity(); if (ctx == null || ctx.helpVariant.equals(ManageCategories.HelpVariant.manage)) { return false; } long cat_id = id; mGroupCursor.moveToPosition(groupPosition); if (mGroupCursor.getInt(mGroupCursor.getColumnIndex(KEY_CHILD_COUNT)) > 0) return false; String label = ((TextView) v.findViewById(R.id.label)).getText().toString(); doSelection(cat_id, label, true); return true; } private void doSelection(long cat_id, String label, boolean isMain) { ManageCategories ctx = (ManageCategories) getActivity(); if (ctx.helpVariant.equals(ManageCategories.HelpVariant.distribution)) { TransactionListDialogFragment .newInstance(mAccount.getId(), cat_id, isMain, mGrouping, buildGroupingClause(), label) .show(getFragmentManager(), TransactionListDialogFragment.class.getName()); return; } Intent intent = new Intent(); intent.putExtra(KEY_CATID, cat_id); intent.putExtra(KEY_LABEL, label); ctx.setResult(ManageCategories.RESULT_OK, intent); ctx.finish(); } public void setGrouping(Grouping grouping) { mGrouping = grouping; mGroupingYear = thisYear; switch (grouping) { case NONE: mGroupingYear = 0; break; case DAY: mGroupingSecond = thisDay; break; case WEEK: mGroupingSecond = thisWeek; break; case MONTH: mGroupingSecond = thisMonth; break; case YEAR: mGroupingSecond = 0; break; } getActivity().supportInvalidateOptionsMenu(); reset(); } public void reset() { //TODO: would be nice to retrieve the same open groups on the next or previous group //the following does not work since the groups will not necessarily stay the same // if (mListView.isGroupExpanded(i)) { // mGroupCursor.moveToPosition(i); // long parentId = mGroupCursor.getLong(mGroupCursor.getColumnIndexOrThrow(KEY_ROWID)); // Bundle bundle = new Bundle(); // bundle.putLong("parent_id", parentId); // mManager.restartLoader(i, bundle, CategoryList.this); // } collapseAll(); mManager.restartLoader(SORTABLE_CURSOR, null, this); mManager.restartLoader(SUM_CURSOR, null, this); mManager.restartLoader(DATEINFO_CURSOR, null, this); } private void updateSum(String prefix, TextView tv, long amount) { if (tv != null) { //noinspection SetTextI18n tv.setText(prefix + Utils.formatCurrency(new Money(mAccount.currency, amount))); } } private void updateColor() { if (bottomLine != null) bottomLine.setBackgroundColor(mAccount.color); } @Override public void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); outState.putSerializable("grouping", mGrouping); outState.putInt("groupingYear", mGroupingYear); outState.putInt("groupingSecond", mGroupingSecond); if (!TextUtils.isEmpty(mFilter)) { outState.putString("filter", mFilter); } } @Override protected void configureMenu(Menu menu, int count, int listId) { ManageCategories ctx = (ManageCategories) getActivity(); if (ctx == null) { return; } boolean inGroup = expandableListSelectionType == ExpandableListView.PACKED_POSITION_TYPE_GROUP; boolean inFilterOrDistribution = ctx.helpVariant.equals(HelpVariant.select_filter) || ctx.helpVariant.equals(HelpVariant.distribution); menu.findItem(R.id.EDIT_COMMAND).setVisible(count == 1 && !inFilterOrDistribution); menu.findItem(R.id.DELETE_COMMAND).setVisible(!inFilterOrDistribution); menu.findItem(R.id.MOVE_COMMAND).setVisible(!inFilterOrDistribution); MenuItem menuItem = menu.findItem(R.id.SELECT_COMMAND); menuItem.setVisible(count == 1 && (ctx.helpVariant.equals(HelpVariant.distribution) || ctx.helpVariant.equals(HelpVariant.select_mapping))); if (ctx.helpVariant.equals(HelpVariant.distribution)) { menuItem.setTitle(R.string.menu_show_transactions); } menu.findItem(R.id.SELECT_COMMAND_MULTIPLE).setVisible(ctx.helpVariant.equals(HelpVariant.select_filter)); menu.findItem(R.id.CREATE_COMMAND).setVisible(inGroup && count == 1 && !inFilterOrDistribution); } @Override protected void configureMenuLegacy(Menu menu, ContextMenu.ContextMenuInfo menuInfo, int listId) { super.configureMenuLegacy(menu, menuInfo, listId); if (expandableListSelectionType == ExpandableListView.PACKED_POSITION_TYPE_GROUP) { ExpandableListContextMenuInfo info = (ExpandableListContextMenuInfo) menuInfo; int groupPos = ExpandableListView.getPackedPositionGroup(info.packedPosition); configureMenuInternal(menu, hasChildren(groupPos)); } } @Override protected void configureMenu11(Menu menu, int count, AbsListView lv) { super.configureMenu11(menu, count, lv); if (expandableListSelectionType == ExpandableListView.PACKED_POSITION_TYPE_GROUP) { SparseBooleanArray checkedItemPositions = mListView.getCheckedItemPositions(); boolean hasChildren = false; for (int i = 0; i < checkedItemPositions.size(); i++) { if (checkedItemPositions.valueAt(i)) { int position = checkedItemPositions.keyAt(i); long pos = mListView.getExpandableListPosition(position); int groupPos = ExpandableListView.getPackedPositionGroup(pos); if (hasChildren(groupPos)) { hasChildren = true; break; } } } configureMenuInternal(menu, hasChildren); } } private boolean hasChildren(int position) { if (position != -1 && mGroupCursor != null) { mGroupCursor.moveToPosition(position); return mGroupCursor.getInt(mGroupCursor.getColumnIndex(KEY_CHILD_COUNT)) > 0; } return false; } private void configureMenuInternal(Menu menu, boolean hasChildren) { menu.findItem(R.id.MOVE_COMMAND).setVisible(!hasChildren); } public void setType(boolean isChecked) { mType = isChecked; reset(); } private void setData(Cursor c, ArrayList<Integer> colors) { chartDisplaysSubs = c != mGroupCursor; ArrayList<Entry> entries1 = new ArrayList<>(); ArrayList<String> xVals = new ArrayList<>(); if (c != null && c.moveToFirst()) { do { long sum = c.getLong(c.getColumnIndex(DatabaseConstants.KEY_SUM)); xVals.add(c.getString(c.getColumnIndex(DatabaseConstants.KEY_LABEL))); entries1.add(new Entry((float) sum, c.getPosition())); } while (c.moveToNext()); PieDataSet ds1 = new PieDataSet(entries1, ""); ds1.setColors(colors); ds1.setSliceSpace(2f); ds1.setDrawValues(false); mChart.setData(new PieData(xVals, ds1)); mChart.getLegend().setEnabled(false); // undo all highlights mChart.highlightValues(null); mChart.invalidate(); } else { mChart.clear(); } } private ArrayList<Integer> getSubColors(int color) { //inspired by http://highintegritydesign.com/tools/tinter-shader/scripts/shader-tinter.js return MyApplication.getThemeType().equals(ThemeType.dark) ? getTints(color) : getShades(color); } private ArrayList<Integer> getShades(int color) { ArrayList<Integer> result = new ArrayList<>(); int red = Color.red(color); int redDecrement = (int) Math.round(red * 0.1); int green = Color.green(color); int greenDecrement = (int) Math.round(green * 0.1); int blue = Color.blue(color); int blueDecrement = (int) Math.round(blue * 0.1); for (int i = 0; i < 10; i++) { red = red - redDecrement; if (red <= 0) { red = 0; } green = green - greenDecrement; if (green <= 0) { green = 0; } blue = blue - blueDecrement; if (blue <= 0) { blue = 0; } result.add(Color.rgb(red, green, blue)); } result.add(Color.BLACK); return result; } private ArrayList<Integer> getTints(int color) { ArrayList<Integer> result = new ArrayList<>(); int red = Color.red(color); int redIncrement = (int) Math.round((255 - red) * 0.1); int green = Color.green(color); int greenIncrement = (int) Math.round((255 - green) * 0.1); int blue = Color.blue(color); int blueIncrement = (int) Math.round((255 - blue) * 0.1); for (int i = 0; i < 10; i++) { red = red + redIncrement; if (red >= 255) { red = 255; } green = green + greenIncrement; if (green >= 255) { red = 255; } blue = blue + blueIncrement; if (blue >= 255) { red = 255; } result.add(Color.rgb(red, green, blue)); } result.add(Color.WHITE); return result; } private void highlight(int position) { mChart.highlightValue(position, 0); if (position != -1) setCenterText(position); } private void setCenterText(int position) { PieData data = mChart.getData(); String description = data.getXVals().get(position); String value = data.getDataSet().getValueFormatter().getFormattedValue( Math.abs(mChart.getPercentOfTotal(data.getDataSet().getEntryForXIndex(position).getVal()))) + " %"; mChart.setCenterText(description + "\n" + value); } }