org.totschnig.myexpenses.fragment.PlanList.java Source code

Java tutorial

Introduction

Here is the source code for org.totschnig.myexpenses.fragment.PlanList.java

Source

/*   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 java.util.ArrayList;
import java.util.Calendar;
import java.util.HashMap;

import org.totschnig.myexpenses.MyApplication;
import org.totschnig.myexpenses.R;
import org.totschnig.myexpenses.activity.ExpenseEdit;
import org.totschnig.myexpenses.activity.ManageTemplates;
import org.totschnig.myexpenses.model.Plan;
import org.totschnig.myexpenses.provider.DbUtils;
import org.totschnig.myexpenses.provider.TransactionProvider;
import org.totschnig.myexpenses.ui.SimpleCursorTreeAdapter;
import org.totschnig.myexpenses.util.Utils;

import android.annotation.SuppressLint;
import android.content.ContentUris;
import android.content.Context;
import android.content.Intent;
import android.database.Cursor;
import android.net.Uri;
import android.os.Bundle;
import android.support.v4.app.LoaderManager;
import android.support.v4.content.CursorLoader;
import android.support.v4.content.Loader;
import android.text.SpannableStringBuilder;
import android.text.TextUtils;
import android.text.style.StyleSpan;
import android.text.style.UnderlineSpan;
import android.util.Log;
import android.view.ContextMenu;
import android.view.LayoutInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.view.ContextMenu.ContextMenuInfo;
import android.widget.ExpandableListView;
import android.widget.ImageView;
import android.widget.TextView;
import android.widget.ExpandableListView.ExpandableListContextMenuInfo;

import com.android.calendar.CalendarContractCompat.Events;
import com.android.calendar.CalendarContractCompat.Instances;

public class PlanList extends BudgetListFragment implements LoaderManager.LoaderCallbacks<Cursor> {
    public static final int TEMPLATES_CURSOR = -1;
    public static final int PLANS_CURSOR = -2;
    Cursor mTemplatesCursor;
    private HashMap<Long, String> mPlanTimeInfo;
    private HashMap<Long, Long> mInstance2TransactionMap = new HashMap<Long, Long>();
    private MyExpandableListAdapter mAdapter;
    //private SimpleCursorAdapter mAdapter;
    //private StickyListHeadersListView mListView;
    int mGroupIdColumnIndex;
    private LoaderManager mManager;

    private int columnIndexPlanId, columnIndexAmount, columnIndexLabelSub, columnIndexComment, columnIndexPayee,
            columnIndexTitle, columnIndexColor, columnIndexTransferPeer, columnIndexCurrency, columnIndexRowId;
    boolean indexesCalculated = false;
    private ExpandableListView mListView;
    private int mExpandedPosition = -1;
    public boolean newPlanEnabled;

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        setColors();
        View v = inflater.inflate(R.layout.plans_list, null, false);
        mListView = (ExpandableListView) v.findViewById(R.id.list);

        mManager = getLoaderManager();
        mManager.initLoader(TEMPLATES_CURSOR, null, this);
        mAdapter = new MyExpandableListAdapter(getActivity(), null, R.layout.plan_row, R.layout.plan_instance_row,
                new String[] { KEY_TITLE, KEY_LABEL_MAIN, KEY_AMOUNT },
                new int[] { R.id.title, R.id.category, R.id.amount }, new String[] { Instances.BEGIN },
                new int[] { R.id.date });
        mListView.setAdapter(mAdapter);
        mListView.setEmptyView(v.findViewById(R.id.empty));
        mListView.setChoiceMode(ExpandableListView.CHOICE_MODE_SINGLE);
        registerForContextMenu(mListView);
        return v;
    }

    @Override
    public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) {
        ExpandableListContextMenuInfo info = (ExpandableListContextMenuInfo) menuInfo;
        int type = ExpandableListView.getPackedPositionType(info.packedPosition);

        // Menu entries relevant only for the group
        if (type == ExpandableListView.PACKED_POSITION_TYPE_GROUP) {
            super.onCreateContextMenu(menu, v, menuInfo);
        } else {
            Long transactionId = mInstance2TransactionMap.get(info.id);
            if (transactionId == null) {
                //state open
                menu.add(0, R.id.CREATE_INSTANCE_SAVE_COMMAND, 0, R.string.menu_apply_template_and_save);
                menu.add(0, R.id.CREATE_INSTANCE_EDIT_COMMAND, 0, R.string.menu_apply_template_and_edit);
                menu.add(0, R.id.CANCEL_PLAN_INSTANCE_COMMAND, 0, R.string.menu_cancel_plan_instance);
            } else if (transactionId == 0L) {
                //state cancelled
                menu.add(0, R.id.RESET_PLAN_INSTANCE_COMMAND, 0, R.string.menu_reset_plan_instance);
            } else {
                //state applied
                menu.add(0, R.id.EDIT_COMMAND, 0, R.string.menu_edit);
                menu.add(0, R.id.CANCEL_PLAN_INSTANCE_COMMAND, 0, R.string.menu_cancel_plan_instance);
                menu.add(0, R.id.RESET_PLAN_INSTANCE_COMMAND, 0, R.string.menu_reset_plan_instance);
            }
        }
    }

    @Override
    public boolean onContextItemSelected(MenuItem item) {
        if (!getUserVisibleHint())
            return false;
        ExpandableListContextMenuInfo info = (ExpandableListContextMenuInfo) item.getMenuInfo();
        if (ExpandableListView
                .getPackedPositionType(info.packedPosition) == ExpandableListView.PACKED_POSITION_TYPE_GROUP)
            return ((ManageTemplates) getActivity()).dispatchCommand(item.getItemId(), info.id);
        int group = ExpandableListView.getPackedPositionGroup(info.packedPosition),
                child = ExpandableListView.getPackedPositionChild(info.packedPosition);
        Cursor c = mAdapter.getChild(group, child);
        long date = c.getLong(c.getColumnIndex(Instances.BEGIN));
        long templateId = mTemplatesCursor.getLong(columnIndexRowId);
        Long transactionId = mInstance2TransactionMap.get(info.id);
        Intent i;
        switch (item.getItemId()) {
        case R.id.CREATE_INSTANCE_EDIT_COMMAND:
            i = new Intent(getActivity(), ExpenseEdit.class);
            i.putExtra("template_id", templateId);
            i.putExtra("instance_id", info.id);
            i.putExtra("instance_date", date);
            startActivity(i);
            return true;
        case R.id.CREATE_INSTANCE_SAVE_COMMAND:
            getActivity().getSupportFragmentManager().beginTransaction()
                    .add(TaskExecutionFragment.newInstance(TaskExecutionFragment.TASK_NEW_FROM_TEMPLATE, templateId,
                            new Long[] { info.id, date }), "ASYNC_TASK")
                    .commit();
            return true;
        case R.id.EDIT_COMMAND:
            i = new Intent(getActivity(), ExpenseEdit.class);
            i.putExtra(KEY_ROWID, transactionId);
            startActivity(i);
            return true;
        case R.id.CANCEL_PLAN_INSTANCE_COMMAND:
            getActivity().getSupportFragmentManager().beginTransaction()
                    .add(TaskExecutionFragment.newInstance(TaskExecutionFragment.TASK_CANCEL_PLAN_INSTANCE, info.id,
                            new Long[] { templateId, transactionId }), "ASYNC_TASK")
                    .commit();
            return true;
        case R.id.RESET_PLAN_INSTANCE_COMMAND:
            getActivity().getSupportFragmentManager().beginTransaction()
                    .add(TaskExecutionFragment.newInstance(TaskExecutionFragment.TASK_RESET_PLAN_INSTANCE, info.id,
                            transactionId), "ASYNC_TASK")
                    .commit();
            mInstance2TransactionMap.remove(info.id);
            return true;
        }
        return false;
    }

    @Override
    public Loader<Cursor> onCreateLoader(int id, Bundle bundle) {
        switch (id) {
        case TEMPLATES_CURSOR:
            return new CursorLoader(getActivity(), TransactionProvider.TEMPLATES_URI, null,
                    KEY_PLANID + " is not null", null, null);
        case PLANS_CURSOR:
            return new CursorLoader(getActivity(), Events.CONTENT_URI,
                    new String[] { Events._ID, Events.DTSTART, Events.RRULE, }, Events._ID + " IN ("
                            + TextUtils.join(",", (ArrayList<Long>) bundle.getSerializable("plans")) + ")",
                    null, null);
        default:
            if (id % 2 == 0) {
                // The ID of the recurring event whose instances you are searching
                // for in the Instances table
                String selection = Instances.EVENT_ID + " = " + bundle.getLong("plan_id");
                // Construct the query with the desired date range.
                Uri.Builder builder = Instances.CONTENT_URI.buildUpon();
                long now = System.currentTimeMillis();
                ContentUris.appendId(builder, now);
                ContentUris.appendId(builder, now + 7776000000L); //90 days
                return new CursorLoader(getActivity(), builder.build(),
                        new String[] { Instances._ID, Instances.BEGIN }, selection, null, null);
            } else {
                return new CursorLoader(getActivity(), TransactionProvider.PLAN_INSTANCE_STATUS_URI,
                        new String[] { KEY_TEMPLATEID, KEY_INSTANCEID, KEY_TRANSACTIONID }, KEY_TEMPLATEID + " = ?",
                        new String[] { String.valueOf(bundle.getLong("template_id")) }, null);
            }
        }
    }

    @SuppressLint("NewApi")
    @Override
    public void onLoadFinished(Loader<Cursor> loader, Cursor c) {
        int id = loader.getId();
        long expandedId = ((ManageTemplates) getActivity()).calledFromCalendarWithId;
        switch (id) {
        case TEMPLATES_CURSOR:
            mTemplatesCursor = c;
            if (!indexesCalculated) {
                columnIndexRowId = c.getColumnIndex(KEY_ROWID);
                columnIndexPlanId = c.getColumnIndex(KEY_PLANID);
                columnIndexAmount = c.getColumnIndex(KEY_AMOUNT);
                columnIndexLabelSub = c.getColumnIndex(KEY_LABEL_SUB);
                columnIndexComment = c.getColumnIndex(KEY_COMMENT);
                columnIndexPayee = c.getColumnIndex(KEY_PAYEE_NAME);
                columnIndexTitle = c.getColumnIndex(KEY_TITLE);
                columnIndexColor = c.getColumnIndex(KEY_COLOR);
                columnIndexTransferPeer = c.getColumnIndex(KEY_TRANSFER_PEER);
                columnIndexCurrency = c.getColumnIndex(KEY_CURRENCY);
                indexesCalculated = true;
            }
            int planCount = mTemplatesCursor.getCount();
            newPlanEnabled = planCount < 3;
            if (planCount > 0) {
                mTemplatesCursor.moveToFirst();
                ArrayList<Long> plans = new ArrayList<Long>();
                long templateId, planId;
                Bundle planBundle = new Bundle();
                while (mTemplatesCursor.isAfterLast() == false) {
                    templateId = mTemplatesCursor.getLong(columnIndexRowId);
                    if (expandedId == templateId) {
                        mExpandedPosition = mTemplatesCursor.getPosition();
                    }
                    Bundle instanceBundle = new Bundle();
                    instanceBundle.putLong("template_id", templateId);
                    //loader for instance2transactionmap
                    int loaderId = mTemplatesCursor.getPosition() * 2 + 1;
                    if (mManager.getLoader(loaderId) != null && !mManager.getLoader(loaderId).isReset()) {
                        mManager.restartLoader(loaderId, instanceBundle, this);
                    } else {
                        mManager.initLoader(loaderId, instanceBundle, this);
                    }
                    if ((planId = mTemplatesCursor.getLong(columnIndexPlanId)) != 0L) {
                        plans.add(planId);
                    }
                    mTemplatesCursor.moveToNext();
                }
                planBundle.putSerializable("plans", plans);
                if (mManager.getLoader(PLANS_CURSOR) != null && !mManager.getLoader(PLANS_CURSOR).isReset()) {
                    mManager.restartLoader(PLANS_CURSOR, planBundle, this);
                } else {
                    mManager.initLoader(PLANS_CURSOR, planBundle, this);
                }
            } else {
                mPlanTimeInfo = new HashMap<Long, String>();
                mAdapter.setGroupCursor(mTemplatesCursor);
            }
            break;
        case PLANS_CURSOR:
            mPlanTimeInfo = new HashMap<Long, String>();
            c.moveToFirst();
            while (c.isAfterLast() == false) {
                mPlanTimeInfo.put(c.getLong(c.getColumnIndex(Events._ID)), Plan.prettyTimeInfo(getActivity(),
                        c.getString(c.getColumnIndex(Events.RRULE)), c.getLong(c.getColumnIndex(Events.DTSTART))));
                c.moveToNext();
            }
            mAdapter.setGroupCursor(mTemplatesCursor);
            if (mExpandedPosition != -1) {
                mListView.expandGroup(mExpandedPosition);
            }
            break;
        default:
            int groupPosition = id / 2;
            if (id % 2 == 0) {
                //check if group still exists
                if (mAdapter.getGroupId(groupPosition) != 0) {
                    mAdapter.setChildrenCursor(groupPosition, c);
                    if (mExpandedPosition != -1) {
                        mListView.setSelectionFromTop(mExpandedPosition, 0);
                    }
                }
            } else {
                c.moveToFirst();
                while (c.isAfterLast() == false) {
                    mInstance2TransactionMap.put(c.getLong(c.getColumnIndex(KEY_INSTANCEID)),
                            c.getLong(c.getColumnIndex(KEY_TRANSACTIONID)));
                    c.moveToNext();
                }
            }
        }
    }

    @Override
    public void onLoaderReset(Loader<Cursor> loader) {
        int id = loader.getId();
        switch (id) {
        case TEMPLATES_CURSOR:
            mTemplatesCursor = null;
            mAdapter.setGroupCursor(null);
        case PLANS_CURSOR:
            mPlanTimeInfo = null;
        default:
            int groupPosition = id / 2;
            if (id % 2 == 0) {
                try {
                    mAdapter.setChildrenCursor(groupPosition, null);
                } catch (NullPointerException e) {
                    Log.w("TAG", "Adapter expired, try again on the next query: " + e.getMessage());
                }
            }
        }
    }

    public class MyExpandableListAdapter extends SimpleCursorTreeAdapter {
        String categorySeparator = " : ", commentSeparator = " / ";
        Calendar calendar = Calendar.getInstance();
        java.text.DateFormat dateFormat = java.text.DateFormat.getDateInstance(java.text.DateFormat.FULL);

        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);
        }

        @Override
        public void setViewText(TextView v, String text) {
            switch (v.getId()) {
            case R.id.date:
                calendar.setTimeInMillis(Long.valueOf(text));
                text = dateFormat.format(calendar.getTime());
            }
            super.setViewText(v, text);
        }

        @Override
        public View getChildView(int groupPosition, int childPosition, boolean isLastChild, View convertView,
                ViewGroup parent) {
            convertView = super.getChildView(groupPosition, childPosition, isLastChild, convertView, parent);
            Cursor c = getChild(groupPosition, childPosition);
            ImageView iv = (ImageView) convertView.findViewById(R.id.planInstanceStatus);
            Long instanceId = c.getLong(c.getColumnIndex(Instances._ID));
            Log.i(MyApplication.TAG, "looking up instance2transactionamp for instance " + instanceId);
            Long transactionId = mInstance2TransactionMap.get(instanceId);
            if (transactionId == null)
                iv.setImageResource(R.drawable.ic_stat_open);
            else if (transactionId == 0L)
                iv.setImageResource(R.drawable.ic_stat_cancelled);
            else
                iv.setImageResource(R.drawable.ic_stat_applied);
            return convertView;
        }

        @Override
        public View getGroupView(int groupPosition, boolean isExpanded, View convertView, ViewGroup parent) {
            convertView = super.getGroupView(groupPosition, isExpanded, convertView, parent);
            Cursor c = getCursor();
            c.moveToPosition(groupPosition);
            TextView tv1 = (TextView) convertView.findViewById(R.id.amount);
            long amount = c.getLong(columnIndexAmount);
            if (amount < 0) {
                tv1.setTextColor(colorExpense);
                // Set the background color of the text.
            } else {
                tv1.setTextColor(colorIncome);
            }
            tv1.setText(Utils.convAmount(amount, Utils.getSaveInstance(c.getString(columnIndexCurrency))));
            Long planId = c.getLong(columnIndexPlanId);
            String planInfo = mPlanTimeInfo.get(planId);
            if (planInfo == null) {
                planInfo = getString(R.string.plan_event_deleted);
            }
            ((TextView) convertView.findViewById(R.id.title))
                    .setText(c.getString(columnIndexTitle) + " (" + planInfo + ")");
            int color = c.getInt(columnIndexColor);
            convertView.findViewById(R.id.colorAccount).setBackgroundColor(color);
            TextView tv2 = (TextView) convertView.findViewById(R.id.category);
            CharSequence catText = tv2.getText();
            if (c.getInt(columnIndexTransferPeer) > 0) {
                catText = ((amount < 0) ? "=> " : "<= ") + catText;
            } else {
                Long catId = DbUtils.getLongOrNull(c, KEY_CATID);
                if (catId == null) {
                    catText = getString(R.string.no_category_assigned);
                } else {
                    String label_sub = c.getString(columnIndexLabelSub);
                    if (label_sub != null && label_sub.length() > 0) {
                        catText = catText + categorySeparator + label_sub;
                    }
                }
            }
            SpannableStringBuilder ssb;
            String comment = c.getString(columnIndexComment);
            if (comment != null && comment.length() > 0) {
                ssb = new SpannableStringBuilder(comment);
                ssb.setSpan(new StyleSpan(android.graphics.Typeface.ITALIC), 0, comment.length(), 0);
                catText = TextUtils.concat(catText, commentSeparator, ssb);
            }
            String payee = c.getString(columnIndexPayee);
            if (payee != null && payee.length() > 0) {
                ssb = new SpannableStringBuilder(payee);
                ssb.setSpan(new UnderlineSpan(), 0, payee.length(), 0);
                catText = TextUtils.concat(catText, commentSeparator, ssb);
            }
            tv2.setText(catText);
            return convertView;
        }

        @Override
        protected Cursor getChildrenCursor(Cursor groupCursor) {
            long planId = groupCursor.getLong(groupCursor.getColumnIndexOrThrow(KEY_PLANID));

            Bundle bundle = new Bundle();
            bundle.putLong("plan_id", planId);
            int groupLoaderId = groupCursor.getPosition() * 2;
            //we use groupPos*2 as id of the calendar instances query
            //and groupPos*2+1 as id of the plan instance status query
            if (mManager.getLoader(groupLoaderId) != null && !mManager.getLoader(groupLoaderId).isReset()) {
                try {
                    mManager.restartLoader(groupLoaderId, bundle, PlanList.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(groupLoaderId, bundle, PlanList.this);
            }
            return null;
        }
    }

    public void refresh() {
        mAdapter.notifyDataSetChanged();
    }

    public void listFocus() {
        mListView.requestFocus();
    }
}