Java tutorial
/* * Copyright (C) 2013 Marten Gajda <marten@dmfs.org> * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package org.dmfs.tasks; import org.dmfs.android.retentionmagic.SupportFragment; import org.dmfs.android.retentionmagic.annotations.Parameter; import org.dmfs.android.retentionmagic.annotations.Retain; import org.dmfs.provider.tasks.TaskContract; import org.dmfs.provider.tasks.TaskContract.Instances; import org.dmfs.provider.tasks.TaskContract.Tasks; import org.dmfs.tasks.groupings.ByDueDate; import org.dmfs.tasks.groupings.ByList; import org.dmfs.tasks.groupings.filters.AbstractFilter; import org.dmfs.tasks.groupings.filters.ConstantFilter; import org.dmfs.tasks.model.Model; import org.dmfs.tasks.model.Sources; import org.dmfs.tasks.utils.ExpandableGroupDescriptor; import org.dmfs.tasks.utils.ExpandableGroupDescriptorAdapter; import org.dmfs.tasks.utils.FlingDetector; import org.dmfs.tasks.utils.FlingDetector.OnFlingListener; import org.dmfs.tasks.utils.OnChildLoadedListener; import org.dmfs.tasks.utils.OnModelLoadedListener; import org.dmfs.tasks.utils.RetainExpandableListView; import org.dmfs.tasks.utils.SearchHistoryDatabaseHelper.SearchHistoryColumns; import android.accounts.Account; import android.accounts.AccountManager; import android.annotation.TargetApi; import android.app.Activity; import android.app.AlertDialog; import android.content.ContentResolver; import android.content.ContentUris; import android.content.ContentValues; import android.content.Context; import android.content.DialogInterface; import android.content.DialogInterface.OnClickListener; import android.content.Intent; import android.content.res.Resources; import android.database.Cursor; import android.net.Uri; import android.os.AsyncTask; import android.os.Build; import android.os.Bundle; import android.os.Handler; import android.support.v4.app.LoaderManager; import android.support.v4.content.Loader; 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.ExpandableListAdapter; import android.widget.ExpandableListView; import android.widget.ExpandableListView.OnChildClickListener; import android.widget.ExpandableListView.OnGroupCollapseListener; import android.widget.ListView; import android.widget.TextView; import android.widget.Toast; /** * A list fragment representing a list of Tasks. This fragment also supports tablet devices by allowing list items to be given an 'activated' state upon * selection. This helps indicate which item is currently being viewed in a {@link ViewTaskFragment}. * <p> * Activities containing this fragment MUST implement the {@link Callbacks} interface * * @author Tobias Reinsch <tobias@dmfs.org> */ public class TaskListFragment extends SupportFragment implements LoaderManager.LoaderCallbacks<Cursor>, OnChildLoadedListener, OnModelLoadedListener, OnFlingListener { @SuppressWarnings("unused") private static final String TAG = "org.dmfs.tasks.TaskListFragment"; private final static String ARG_INSTANCE_ID = "instance_id"; private final static String ARG_TWO_PANE_LAYOUT = "two_pane_layout"; private static final long INTERVAL_LISTVIEW_REDRAW = 60000; /** * A filter to hide completed tasks. */ private final static AbstractFilter COMPLETED_FILTER = new ConstantFilter(Tasks.IS_CLOSED + "=0"); /** * The group descriptor to use. At present this can be either {@link ByDueDate#GROUP_DESCRIPTOR}, {@link ByCompleted#GROUP_DESCRIPTOR} or * {@link ByList#GROUP_DESCRIPTOR}. */ private ExpandableGroupDescriptor mGroupDescriptor; /** * The fragment's current callback object, which is notified of list item clicks. */ private Callbacks mCallbacks; @Retain(permanent = true, instanceNSField = "mInstancePosition") private int mActivatedPositionGroup = ExpandableListView.INVALID_POSITION; @Retain(permanent = true, instanceNSField = "mInstancePosition") private int mActivatedPositionChild = ExpandableListView.INVALID_POSITION; private RetainExpandableListView mExpandableListView; private Context mAppContext; private ExpandableGroupDescriptorAdapter mAdapter; private Handler mHandler; @Retain(permanent = true, instanceNSField = "mInstancePosition") private long[] mSavedExpandedGroups = null; @Retain(permanent = true, instanceNSField = "mInstancePosition") private boolean mSavedCompletedFilter; @Parameter(key = ARG_INSTANCE_ID) private int mInstancePosition; @Parameter(key = ARG_TWO_PANE_LAYOUT) private boolean mTwoPaneLayout; private Loader<Cursor> mCursorLoader; private String mAuthority; private Uri mSelectedTaskUri; /** The child position to open when the fragment is displayed. **/ private ListPosition mSelectedChildPosition; @Retain private int mPageId = -1; private final OnChildClickListener mTaskItemClickListener = new OnChildClickListener() { @Override public boolean onChildClick(ExpandableListView parent, View v, int groupPosition, int childPosition, long id) { selectChildView(parent, groupPosition, childPosition, true); if (mExpandableListView.getChoiceMode() == ExpandableListView.CHOICE_MODE_SINGLE) { mActivatedPositionGroup = groupPosition; mActivatedPositionChild = childPosition; } /* * In contrast to a ListView an ExpandableListView does not set the activated item on it's own. So we have to do that here. */ setActivatedItem(groupPosition, childPosition); return true; } }; private final OnGroupCollapseListener mTaskListCollapseListener = new OnGroupCollapseListener() { @Override public void onGroupCollapse(int groupPosition) { if (groupPosition == mActivatedPositionGroup) { mActivatedPositionChild = ExpandableListView.INVALID_POSITION; mActivatedPositionGroup = ExpandableListView.INVALID_POSITION; } } }; /** * A callback interface that all activities containing this fragment must implement. This mechanism allows activities to be notified of item selections. */ public interface Callbacks { /** * Callback for when an item has been selected. * * @param taskUri * The {@link Uri} of the selected task. * @param forceReload * Whether to reload the task or not. * @param sender * The sender of the callback. */ public void onItemSelected(Uri taskUri, boolean forceReload, int pagePosition); public ExpandableGroupDescriptor getGroupDescriptor(int position); public void onAddNewTask(); } /** * A runnable that periodically updates the list. We need that to update relative dates & times. TODO: we probably should move that to the adapter to update * only the date & times fields, not the entire list. */ private Runnable mListRedrawRunnable = new Runnable() { @Override public void run() { mExpandableListView.invalidateViews(); mHandler.postDelayed(this, INTERVAL_LISTVIEW_REDRAW); } }; public static TaskListFragment newInstance(int instancePosition, boolean twoPaneLayout) { TaskListFragment result = new TaskListFragment(); Bundle args = new Bundle(); args.putInt(ARG_INSTANCE_ID, instancePosition); args.putBoolean(ARG_TWO_PANE_LAYOUT, twoPaneLayout); result.setArguments(args); return result; } /** * Mandatory empty constructor for the fragment manager to instantiate the fragment (e.g. upon screen orientation changes). */ public TaskListFragment() { } @Override public void onAttach(Activity activity) { super.onAttach(activity); mAuthority = getString(R.string.org_dmfs_tasks_authority); mAppContext = activity.getBaseContext(); // Activities containing this fragment must implement its callbacks. if (!(activity instanceof Callbacks)) { throw new IllegalStateException("Activity must implement fragment's callbacks."); } mCallbacks = (Callbacks) activity; // load accounts early Sources.loadModelAsync(activity, TaskContract.LOCAL_ACCOUNT_TYPE, this); } @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); mHandler = new Handler(); setHasOptionsMenu(true); } @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View rootView = inflater.inflate(R.layout.fragment_expandable_task_list, container, false); mExpandableListView = (RetainExpandableListView) rootView.findViewById(android.R.id.list); if (mGroupDescriptor == null) { loadGroupDescriptor(); } // setup the views this.prepareReload(); // expand lists if (mSavedExpandedGroups != null) { mExpandableListView.expandGroups(mSavedExpandedGroups); } FlingDetector swiper = new FlingDetector(mExpandableListView, mGroupDescriptor.getElementViewDescriptor().getFlingContentViewId(), getActivity().getApplicationContext()); swiper.setOnFlingListener(this); return rootView; } @Override public void onViewCreated(View view, Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); } @Override public void onStart() { reloadCursor(); super.onStart(); } @Override public void onResume() { super.onResume(); mExpandableListView.invalidateViews(); startAutomaticRedraw(); openSelectedChild(); if (mTwoPaneLayout) { setListViewScrollbarPositionLeft(true); setActivateOnItemClick(true); } } @Override public void onPause() { // we can't rely on save instance state being called before onPause, so we get the expanded groups here again if (!((TaskListActivity) getActivity()).isInTransientState()) { mSavedExpandedGroups = mExpandableListView.getExpandedGroups(); } stopAutomaticRedraw(); super.onPause(); } @Override public void onDetach() { super.onDetach(); } @Override public void onSaveInstanceState(Bundle outState) { if (!((TaskListActivity) getActivity()).isInTransientState()) { mSavedExpandedGroups = mExpandableListView.getExpandedGroups(); } super.onSaveInstanceState(outState); } @TargetApi(Build.VERSION_CODES.HONEYCOMB) @Override public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { // create menu inflater.inflate(R.menu.task_list_fragment_menu, menu); // restore menu state MenuItem item = menu.findItem(R.id.menu_show_completed); if (item != null) { item.setChecked(mSavedCompletedFilter); if (android.os.Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB) { if (mSavedCompletedFilter) { item.setTitle(R.string.menu_hide_completed); } else { item.setTitle(R.string.menu_show_completed); } } } } @Override public boolean onOptionsItemSelected(MenuItem item) { int itemId = item.getItemId(); if (itemId == R.id.menu_add_task) { mCallbacks.onAddNewTask(); return true; } else if (itemId == R.id.menu_show_completed) { mSavedCompletedFilter = !mSavedCompletedFilter; item.setChecked(mSavedCompletedFilter); mAdapter.setChildCursorFilter(mSavedCompletedFilter ? null : COMPLETED_FILTER); // reload the child cursors only for (int i = 0; i < mAdapter.getGroupCount(); ++i) { mAdapter.reloadGroup(i); } return true; } else if (itemId == R.id.menu_sync_now) { doSyncNow(); return true; } else { return super.onOptionsItemSelected(item); } } @Override public Loader<Cursor> onCreateLoader(int arg0, Bundle arg1) { if (mGroupDescriptor != null) { mCursorLoader = mGroupDescriptor.getGroupCursorLoader(mAppContext); } return mCursorLoader; } @Override public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) { if (mSavedExpandedGroups == null) { mSavedExpandedGroups = mExpandableListView.getExpandedGroups(); } mAdapter.setGroupCursor(cursor); if (mSavedExpandedGroups != null) { mExpandableListView.expandGroups(mSavedExpandedGroups); if (!((TaskListActivity) getActivity()).isInTransientState()) { mSavedExpandedGroups = null; } } mHandler.post(new Runnable() { @Override public void run() { mAdapter.reloadLoadedGroups(); } }); } @Override public void onLoaderReset(Loader<Cursor> loader) { mAdapter.changeCursor(null); } @Override public void onChildLoaded(final int pos, Cursor childCursor) { if (mActivatedPositionChild != ExpandableListView.INVALID_POSITION) { if (pos == mActivatedPositionGroup && mActivatedPositionChild != ExpandableListView.INVALID_POSITION) { mHandler.post(setOpenHandler); } } // check for child to select if (mTwoPaneLayout) { selectChild(pos, childCursor); } } @Override public void onModelLoaded(Model model) { // nothing to do, we've just loaded the default model to speed up loading the detail view and the editor view. } private void selectChildView(ExpandableListView expandLV, int groupPosition, int childPosition, boolean force) { if (groupPosition < mAdapter.getGroupCount() && childPosition < mAdapter.getChildrenCount(groupPosition)) { // a task instance element has been clicked, get it's instance id and notify the activity ExpandableListAdapter listAdapter = expandLV.getExpandableListAdapter(); Cursor cursor = (Cursor) listAdapter.getChild(groupPosition, childPosition); if (cursor == null) { return; } // TODO: for now we get the id of the task, not the instance, once we support recurrence we'll have to change that Long selectTaskId = cursor.getLong(cursor.getColumnIndex(Instances.TASK_ID)); if (selectTaskId != null) { // Notify the active callbacks interface (the activity, if the fragment is attached to one) that an item has been selected. // TODO: use the instance URI one we support recurrence Uri taskUri = ContentUris.withAppendedId(Tasks.getContentUri(mAuthority), selectTaskId); mCallbacks.onItemSelected(taskUri, force, mInstancePosition); } } } /** * prepares the update of the view after the group descriptor was changed * * */ public void prepareReload() { mAdapter = new ExpandableGroupDescriptorAdapter(getActivity(), getLoaderManager(), mGroupDescriptor); mExpandableListView.setAdapter(mAdapter); mExpandableListView.setOnChildClickListener( (android.widget.ExpandableListView.OnChildClickListener) mTaskItemClickListener); mExpandableListView.setOnGroupCollapseListener( (android.widget.ExpandableListView.OnGroupCollapseListener) mTaskListCollapseListener); mAdapter.setOnChildLoadedListener(this); mAdapter.setChildCursorFilter(COMPLETED_FILTER); restoreFilterState(); } private void reloadCursor() { getLoaderManager().restartLoader(-1, null, this); } public void restoreFilterState() { if (mSavedCompletedFilter) { mAdapter.setChildCursorFilter(mSavedCompletedFilter ? null : COMPLETED_FILTER); // reload the child cursors only for (int i = 0; i < mAdapter.getGroupCount(); ++i) { mAdapter.reloadGroup(i); } } } /** * Trigger a synchronization for all accounts. */ private void doSyncNow() { AccountManager accountManager = AccountManager.get(mAppContext); Account[] accounts = accountManager.getAccounts(); for (Account account : accounts) { // TODO: do we need a new bundle for each account or can we reuse it? Bundle extras = new Bundle(); extras.putBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, true); ContentResolver.requestSync(account, mAuthority, extras); } } /** * Remove the task with the given {@link Uri} and title, asking for confirmation first. * * @param taskUri * The {@link Uri} of the atsk to remove. * @param taskTitle * the title of the task to remove. * @return */ private void removeTask(final Uri taskUri, final String taskTitle) { new AlertDialog.Builder(getActivity()).setTitle(R.string.confirm_delete_title).setCancelable(true) .setNegativeButton(android.R.string.cancel, new OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { // nothing to do here } }).setPositiveButton(android.R.string.ok, new OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { // TODO: remove the task in a background task mAppContext.getContentResolver().delete(taskUri, null, null); Toast.makeText(mAppContext, getString(R.string.toast_task_deleted, taskTitle), Toast.LENGTH_SHORT).show(); } }).setMessage(getString(R.string.confirm_delete_message_with_title, taskTitle)).create().show(); } /** * Opens the task editor for the selected Task. * * @param taskUri * The {@link Uri} of the task. * @param taskTitle * The name/title of the task. */ private void openTaskEditor(final Uri taskUri, final String accountType) { Intent editTaskIntent = new Intent(Intent.ACTION_EDIT); editTaskIntent.setData(taskUri); editTaskIntent.putExtra(EditTaskActivity.EXTRA_DATA_ACCOUNT_TYPE, accountType); startActivity(editTaskIntent); } @Override public int canFling(ListView v, int pos) { long packedPos = mExpandableListView.getExpandableListPosition(pos); if (packedPos != ExpandableListView.PACKED_POSITION_VALUE_NULL && ExpandableListView .getPackedPositionType(packedPos) == ExpandableListView.PACKED_POSITION_TYPE_CHILD) { return FlingDetector.RIGHT_FLING | FlingDetector.LEFT_FLING; } else { return 0; } } @Override public void onFlingStart(ListView listView, View listElement, int position, int direction) { // control the visibility of the views that reveal behind a flinging element regarding the fling direction int rightFlingViewId = mGroupDescriptor.getElementViewDescriptor().getFlingRevealRightViewId(); int leftFlingViewId = mGroupDescriptor.getElementViewDescriptor().getFlingRevealLeftViewId(); TextView rightFlingView = null; TextView leftFlingView = null; if (rightFlingViewId != -1) { rightFlingView = (TextView) listElement.findViewById(rightFlingViewId); } if (leftFlingViewId != -1) { leftFlingView = (TextView) listElement.findViewById(leftFlingViewId); } Resources resources = getActivity().getResources(); // change title and icon regarding the task status long packedPos = mExpandableListView.getExpandableListPosition(position); if (ExpandableListView.getPackedPositionType(packedPos) == ExpandableListView.PACKED_POSITION_TYPE_CHILD) { ExpandableListAdapter listAdapter = mExpandableListView.getExpandableListAdapter(); Cursor cursor = (Cursor) listAdapter.getChild(ExpandableListView.getPackedPositionGroup(packedPos), ExpandableListView.getPackedPositionChild(packedPos)); if (cursor != null) { int taskStatus = cursor.getInt(cursor.getColumnIndex(Instances.STATUS)); if (leftFlingView != null && rightFlingView != null) { if (taskStatus == Instances.STATUS_COMPLETED) { leftFlingView.setText(R.string.fling_task_delete); leftFlingView.setCompoundDrawablesWithIntrinsicBounds( resources.getDrawable(R.drawable.content_discard), null, null, null); rightFlingView.setText(R.string.fling_task_uncomplete); rightFlingView.setCompoundDrawablesWithIntrinsicBounds(null, null, resources.getDrawable(R.drawable.content_remove_light), null); } else { leftFlingView.setText(R.string.fling_task_complete); leftFlingView.setCompoundDrawablesWithIntrinsicBounds( resources.getDrawable(R.drawable.ic_action_complete), null, null, null); rightFlingView.setText(R.string.fling_task_edit); rightFlingView.setCompoundDrawablesWithIntrinsicBounds(null, null, resources.getDrawable(R.drawable.content_edit), null); } } } } if (rightFlingView != null) { rightFlingView.setVisibility(direction != FlingDetector.LEFT_FLING ? View.GONE : View.VISIBLE); } if (leftFlingView != null) { leftFlingView.setVisibility(direction != FlingDetector.RIGHT_FLING ? View.GONE : View.VISIBLE); } } @Override public boolean onFlingEnd(ListView v, View listElement, int pos, int direction) { long packedPos = mExpandableListView.getExpandableListPosition(pos); if (ExpandableListView.getPackedPositionType(packedPos) == ExpandableListView.PACKED_POSITION_TYPE_CHILD) { ExpandableListAdapter listAdapter = mExpandableListView.getExpandableListAdapter(); Cursor cursor = (Cursor) listAdapter.getChild(ExpandableListView.getPackedPositionGroup(packedPos), ExpandableListView.getPackedPositionChild(packedPos)); if (cursor != null) { // TODO: for now we get the id of the task, not the instance, once we support recurrence we'll have to change that Long taskId = cursor.getLong(cursor.getColumnIndex(Instances.TASK_ID)); if (taskId != null) { boolean closed = cursor.getLong(cursor.getColumnIndex(Instances.IS_CLOSED)) > 0; String title = cursor.getString(cursor.getColumnIndex(Instances.TITLE)); // TODO: use the instance URI once we support recurrence Uri taskUri = ContentUris.withAppendedId(Tasks.getContentUri(mAuthority), taskId); if (direction == FlingDetector.RIGHT_FLING) { if (closed) { removeTask(taskUri, title); // we do not know for sure if the task has been removed since the user is asked for confirmation first, so return false return false; } else { return setCompleteTask(taskUri, title, true); } } else if (direction == FlingDetector.LEFT_FLING) { if (closed) { return setCompleteTask(taskUri, title, false); } else { openTaskEditor(taskUri, cursor.getString(cursor.getColumnIndex(Instances.ACCOUNT_TYPE))); return false; } } } } } return false; } @Override public void onFlingCancel(int direction) { // TODO Auto-generated method stub } public void loadGroupDescriptor() { if (getActivity() != null) { TaskListActivity activity = (TaskListActivity) getActivity(); if (activity != null) { mGroupDescriptor = activity.getGroupDescriptor(mPageId); } } } /** * Starts the automatic list view redraw (e.g. to display changing time values) on the next minute. */ public void startAutomaticRedraw() { long now = System.currentTimeMillis(); long millisToInterval = INTERVAL_LISTVIEW_REDRAW - (now % INTERVAL_LISTVIEW_REDRAW); mHandler.postDelayed(mListRedrawRunnable, millisToInterval); } /** * Stops the automatic list view redraw. * */ public void stopAutomaticRedraw() { mHandler.removeCallbacks(mListRedrawRunnable); } public int getOpenChildPosition() { return mActivatedPositionChild; } public int getOpenGroupPosition() { return mActivatedPositionGroup; } /** * Turns on activate-on-click mode. When this mode is on, list items will be given the 'activated' state when touched. * <p> * Note: this does not work 100% with {@link ExpandableListView}, it doesn't check touched items automatically. * </p> * * @param activateOnItemClick * Whether to enable single choice mode or not. */ public void setActivateOnItemClick(boolean activateOnItemClick) { mExpandableListView .setChoiceMode(activateOnItemClick ? ListView.CHOICE_MODE_SINGLE : ListView.CHOICE_MODE_NONE); } @TargetApi(Build.VERSION_CODES.HONEYCOMB) public void setListViewScrollbarPositionLeft(boolean left) { if (android.os.Build.VERSION.SDK_INT >= 11) { if (left) { mExpandableListView.setVerticalScrollbarPosition(View.SCROLLBAR_POSITION_LEFT); // expandLV.setScrollBarStyle(style); } else { mExpandableListView.setVerticalScrollbarPosition(View.SCROLLBAR_POSITION_RIGHT); } } } public void setExpandableGroupDescriptor(ExpandableGroupDescriptor groupDescriptor) { mGroupDescriptor = groupDescriptor; } /** * Mark the given task as completed. * * @param taskUri * The {@link Uri} of the task. * @param taskTitle * The name/title of the task. * @param completedValue * The value to be set for the completed status. * @return <code>true</code> if the operation was successful, <code>false</code> otherwise. */ private boolean setCompleteTask(Uri taskUri, String taskTitle, boolean completedValue) { ContentValues values = new ContentValues(); values.put(Tasks.STATUS, completedValue ? Tasks.STATUS_COMPLETED : Tasks.STATUS_IN_PROCESS); if (!completedValue) { values.put(Tasks.PERCENT_COMPLETE, 50); } boolean completed = mAppContext.getContentResolver().update(taskUri, values, null, null) != 0; if (completed) { if (completedValue) { Toast.makeText(mAppContext, getString(R.string.toast_task_completed, taskTitle), Toast.LENGTH_SHORT) .show(); } else { Toast.makeText(mAppContext, getString(R.string.toast_task_uncompleted, taskTitle), Toast.LENGTH_SHORT).show(); } } return completed; } public void setOpenChildPosition(int openChildPosition) { mActivatedPositionChild = openChildPosition; } public void setOpenGroupPosition(int openGroupPosition) { mActivatedPositionGroup = openGroupPosition; } public void notifyDataSetChanged(boolean expandFirst) { getLoaderManager().restartLoader(-1, null, this); } Runnable setOpenHandler = new Runnable() { @Override public void run() { selectChildView(mExpandableListView, mActivatedPositionGroup, mActivatedPositionChild, false); mExpandableListView.expandGroups(mSavedExpandedGroups); setActivatedItem(mActivatedPositionGroup, mActivatedPositionChild); } }; public void setActivatedItem(int groupPosition, int childPosition) { if (groupPosition != ExpandableListView.INVALID_POSITION && groupPosition < mAdapter.getGroupCount() && childPosition != ExpandableListView.INVALID_POSITION && childPosition < mAdapter.getChildrenCount(groupPosition)) { try { mExpandableListView.setItemChecked(mExpandableListView.getFlatListPosition( ExpandableListView.getPackedPositionForChild(groupPosition, childPosition)), true); } catch (NullPointerException e) { // for now we just catch the NPE until we've found the reason // just catching it won't hurt, it's just that the list selection won't be updated properly // FIXME: find the actual cause and fix it } } } public void expandCurrentSearchGroup() { if (mPageId == R.id.task_group_search && mAdapter.getGroupCount() > 0) { Cursor c = mAdapter.getGroup(0); if (c != null && c.getInt(c.getColumnIndex(SearchHistoryColumns.HISTORIC)) < 1) { mExpandableListView.expandGroup(0); } } } public void setPageId(int pageId) { mPageId = pageId; } private void selectChild(final int groupPosition, Cursor childCursor) { mSelectedTaskUri = ((TaskListActivity) getActivity()).getSelectedTaskUri(); if (mSelectedTaskUri != null) { new AsyncSelectChildTask() .execute(new SelectChildTaskParams(groupPosition, childCursor, mSelectedTaskUri)); } } public void openSelectedChild() { if (mSelectedChildPosition != null) { // post delayed to allow the list view to finish creation mExpandableListView.postDelayed(new Runnable() { @Override public void run() { mSelectedChildPosition.flatListPosition = mExpandableListView.getFlatListPosition( RetainExpandableListView.getPackedPositionForChild(mSelectedChildPosition.groupPosition, mSelectedChildPosition.childPosition)); mExpandableListView.expandGroup(mSelectedChildPosition.groupPosition); setActivatedItem(mSelectedChildPosition.groupPosition, mSelectedChildPosition.childPosition); selectChildView(mExpandableListView, mSelectedChildPosition.groupPosition, mSelectedChildPosition.childPosition, true); mExpandableListView.smoothScrollToPosition(mSelectedChildPosition.flatListPosition); } }, 0); } } /** Returns the position of the task in the cursor. Returns -1 if the task is not in the cursor **/ private int getSelectedChildPostion(Uri taskUri, Cursor listCursor) { if (taskUri != null && listCursor != null && listCursor.moveToFirst()) { Long taskIdToSelect = Long.valueOf(taskUri.getLastPathSegment()); do { Long taskId = listCursor.getLong(listCursor.getColumnIndex(Tasks._ID)); if (taskId.equals(taskIdToSelect)) { return listCursor.getPosition(); } } while (listCursor.moveToNext()); } return -1; } private static class SelectChildTaskParams { int groupPosition; Uri taskUriToSelect; Cursor childCursor; SelectChildTaskParams(int groupPosition, Cursor childCursor, Uri taskUriToSelect) { this.groupPosition = groupPosition; this.childCursor = childCursor; this.taskUriToSelect = taskUriToSelect; } } private static class ListPosition { int groupPosition; int childPosition; int flatListPosition; ListPosition(int groupPosition, int childPosition) { this.groupPosition = groupPosition; this.childPosition = childPosition; } } private class AsyncSelectChildTask extends AsyncTask<SelectChildTaskParams, Void, Void> { @Override protected Void doInBackground(SelectChildTaskParams... params) { int count = params.length; for (int i = 0; i < count; i++) { final SelectChildTaskParams param = params[i]; final int childPosition = getSelectedChildPostion(param.taskUriToSelect, param.childCursor); if (childPosition > -1) { mSelectedChildPosition = new ListPosition(param.groupPosition, childPosition); openSelectedChild(); } } return null; } } }