[Java for Android App Development] Alwaysup:
I make an application for Android and I'm using CursorTreeAdapter as ExpandableListView. Now I want to use search box for display the filtered ExpandableListView items. Like this:

Here's the code what I've so far:

MainActivity.java:

package com.example.android.exlistexample;

import java.util.HashMap;

import android.app.SearchManager;
import android.content.Context;
import android.database.ContentObserver;
import android.database.Cursor;
import android.database.DatabaseUtils;
import android.os.Bundle;
import android.os.Handler;
import android.support.v4.app.LoaderManager;
import android.support.v4.app.LoaderManager.LoaderCallbacks;
import android.support.v4.content.CursorLoader;
import android.support.v4.content.Loader;
import android.util.Log;
import android.widget.ExpandableListView;
import android.widget.SearchView;
import android.widget.SearchView.OnCloseListener;
import android.widget.SearchView.OnQueryTextListener;

import com.actionbarsherlock.app.SherlockFragmentActivity;
import com.example.android.exlistexample.provider.CfpContract;

public class MainActivity extends SherlockFragmentActivity {

private SearchView search;
private MyListAdapter listAdapter;
private ExpandableListView myList;

private final String DEBUG_TAG = getClass().getSimpleName().toString();

@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);

SearchManager searchManager = (SearchManager) getSystemService(Context.SEARCH_SERVICE);
search = (SearchView) findViewById(R.id.search);
search.setSearchableInfo(searchManager
.getSearchableInfo(getComponentName()));
search.setIconifiedByDefault(false);
search.setOnQueryTextListener(new OnQueryTextListener() {

@Override
public boolean onQueryTextSubmit(String query) {
listAdapter.filterData(query);
expandAll();
return false;
}

@Override
public boolean onQueryTextChange(String query) {
listAdapter.filterData(query);
expandAll();
return false;
}
});

search.setOnCloseListener(new OnCloseListener() {

@Override
public boolean onClose() {
listAdapter.filterData("");
expandAll();
return false;
}
});

// get reference to the ExpandableListView
myList = (ExpandableListView) findViewById(R.id.expandableList);
// create the adapter
listAdapter = new MyListAdapter(null, MainActivity.this);
// attach the adapter to the list
myList.setAdapter(listAdapter);

Loader<Cursor> loader = getSupportLoaderManager().getLoader(-1);
if (loader != null && !loader.isReset()) {
runOnUiThread(new Runnable() {
public void run() {
getSupportLoaderManager().restartLoader(-1, null,
mSpeakersLoaderCallback);
}
});
} else {
runOnUiThread(new Runnable() {
public void run() {
getSupportLoaderManager().initLoader(-1, null,
mSpeakersLoaderCallback).forceLoad();
;
}
});
}

}

@Override
public void onResume() {
super.onResume();

getApplicationContext().getContentResolver().registerContentObserver(
CfpContract.Groups.CONTENT_URI, true, mSpeakerChangesObserver);
}

@Override
public void onPause() {
super.onPause();

getApplicationContext().getContentResolver().unregisterContentObserver(
mSpeakerChangesObserver);
}

// method to expand all groups
private void expandAll() {
int count = listAdapter.getGroupCount();
for (int i = 0; i < count; i++) {
myList.expandGroup(i);
}
}

public LoaderManager.LoaderCallbacks<Cursor> mSpeakersLoaderCallback = new LoaderCallbacks<Cursor>() {

@Override
public Loader<Cursor> onCreateLoader(int id, Bundle args) {
Log.d(DEBUG_TAG, "onCreateLoader for loader_id " + id);
CursorLoader cl = null;
if (id != -1) {
if (id == 7) {
cl = new CursorLoader(getApplicationContext(),
CfpContract.Classes.CONTENT_URI,
MyListAdapter.CLASSES_PROJECTION, null, null,
CfpContract.Classes.DEFAULT_SORT);
} else if (id == 8) {
cl = new CursorLoader(getApplicationContext(),
CfpContract.Teachers.CONTENT_URI,
MyListAdapter.TEACHERS_PROJECTION, null, null,
CfpContract.Teachers.DEFAULT_SORT);
}
} else {
// group cursor
cl = new CursorLoader(getApplicationContext(),
CfpContract.Groups.CONTENT_URI,
MyListAdapter.GROUPS_PROJECTION, null, null,
CfpContract.Groups.DEFAULT_SORT);
}

return cl;
}

@Override
public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
// Swap the new cursor in.
int id = loader.getId();
Log.d("Dump Cursor MainActivity",
DatabaseUtils.dumpCursorToString(data));
Log.d(DEBUG_TAG, "onLoadFinished() for loader_id " + id);
if (id != -1) {
// child cursor
if (!data.isClosed()) {
Log.d(DEBUG_TAG, "data.getCount() " + data.getCount());

HashMap<Integer, Integer> groupMap = listAdapter
.getGroupMap();
try {
int groupPos = groupMap.get(id);
Log.d(DEBUG_TAG, "onLoadFinished() for groupPos "
+ groupPos);
listAdapter.setChildrenCursor(groupPos, data);
} catch (NullPointerException e) {
Log.w("DEBUG",
"Adapter expired, try again on the next query: "
+ e.getMessage());
}
}
} else {
listAdapter.setGroupCursor(data);
}
}

@Override
public void onLoaderReset(Loader<Cursor> loader) {
// This is called when the last Cursor provided to onLoadFinished()
// is about to be closed.
int id = loader.getId();
Log.d(DEBUG_TAG, "onLoaderReset() for loader_id " + id);
if (id != 1) {
// child cursor
try {
listAdapter.setChildrenCursor(id, null);
} catch (NullPointerException e) {
Log.w(DEBUG_TAG,
"Adapter expired, try again on the next query: "
+ e.getMessage());
}
} else {
listAdapter.setGroupCursor(null);
}
}
};

private ContentObserver mSpeakerChangesObserver = new ContentObserver(
new Handler()) {

@Override
public void onChange(boolean selfChange) {
if (getApplicationContext() != null) {
runOnUiThread(new Runnable() {
public void run() {
getSupportLoaderManager().restartLoader(-1, null,
mSpeakersLoaderCallback);
}
});
}
}
};
}

MyListAdapter.java:

package com.example.android.exlistexample;

import java.util.HashMap;

import android.content.Context;
import android.database.Cursor;
import android.provider.BaseColumns;
import android.support.v4.content.Loader;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.CursorTreeAdapter;
import android.widget.ImageView;
import android.widget.TextView;

import com.example.android.exlistexample.provider.CfpContract;
import com.example.android.exlistexample.provider.CfpContract.Groups;
import com.joanzapata.android.iconify.Iconify;
import com.joanzapata.android.iconify.Iconify.IconValue;

public class MyListAdapter extends CursorTreeAdapter {

public HashMap<String, View> childView = new HashMap<String, View>();

/**
* The columns we are interested in from the database
*/
protected static final String[] GROUPS_PROJECTION = new String[] {
BaseColumns._ID, CfpContract.Groups.GROUP_ID,
CfpContract.Groups.GROUP_NAME, CfpContract.Groups.GROUP_IMAGE, };

protected static final String[] CLASSES_PROJECTION = new String[] {
BaseColumns._ID, CfpContract.Classes.CLASS_ID,
CfpContract.Classes.CLASS_NAME, CfpContract.Classes.CLASS_CAT, };

protected static final String[] TEACHERS_PROJECTION = new String[] {
BaseColumns._ID, CfpContract.Teachers.TEACHER_ID,
CfpContract.Teachers.TEACHER_NAME,
CfpContract.Teachers.TEACHER_SHORT,
CfpContract.Teachers.TEACHER_OPLEIDINGEN, };

private final String DEBUG_TAG = getClass().getSimpleName().toString();

protected final HashMap<Integer, Integer> mGroupMap;

private MainActivity mActivity;
private LayoutInflater mInflater;

public MyListAdapter(Cursor cursor, Context context) {

super(cursor, context);
mActivity = (MainActivity) context;
mInflater = LayoutInflater.from(context);
mGroupMap = new HashMap<Integer, Integer>();
}

@Override
public View newGroupView(Context context, Cursor cursor,
boolean isExpanded, ViewGroup parent) {

final View view = mInflater.inflate(R.layout.list_group, parent, false);
return view;
}

@Override
public void bindGroupView(View view, Context context, Cursor cursor,
boolean isExpanded) {

TextView lblListHeader = (TextView) view
.findViewById(R.id.lblListHeader);

if (lblListHeader != null) {
lblListHeader.setText(cursor.getString(cursor
.getColumnIndex(Groups.GROUP_NAME)));

ImageView groupIcon = (ImageView) view
.findViewById(R.id.lblListHeaderIcon);
groupIcon.setImageResource(cursor.getInt(cursor
.getColumnIndex(Groups.GROUP_IMAGE)));
}

TextView IndicatorText = (TextView) view
.findViewById(R.id.lblListHeaderIndicator);

if (IndicatorText != null) {
if (isExpanded) {
Iconify.setIcon(IndicatorText, IconValue.icon_caret_up);
} else {
Iconify.setIcon(IndicatorText, IconValue.icon_caret_down);
}
}
}

@Override
public View newChildView(Context context, Cursor cursor,
boolean isLastChild, ViewGroup parent) {

final View view = mInflater.inflate(R.layout.list_item, parent, false);

return view;
}

@Override
public void bindChildView(View view, Context context, Cursor cursor,
boolean isLastChild) {

TextView txtListChild = (TextView) view.findViewById(R.id.lblListItem);

txtListChild.setText(cursor.getString(2));

}

protected Cursor getChildrenCursor(Cursor groupCursor) {
// Given the group, we return a cursor for all the children within that
// group
int groupPos = groupCursor.getPosition();
int groupId = groupCursor.getInt(groupCursor
.getColumnIndex(BaseColumns._ID));

Log.d(DEBUG_TAG, "getChildrenCursor() for groupPos " + groupPos);
Log.d(DEBUG_TAG, "getChildrenCursor() for groupId " + groupId);

mGroupMap.put(groupId, groupPos);

Loader loader = mActivity.getSupportLoaderManager().getLoader(groupId);
if (loader != null && !loader.isReset()) {
mActivity.getSupportLoaderManager().restartLoader(groupId, null,
mActivity.mSpeakersLoaderCallback);
} else {
mActivity.getSupportLoaderManager().initLoader(groupId, null,
mActivity.mSpeakersLoaderCallback);
}

return null;
}

// Access method
public HashMap<Integer, Integer> getGroupMap() {
return mGroupMap;
}

public void filterData(String query) {
// TODO Filter the data here
}
}

I have very considerably simplified and cleaned the code (so that you guys that not need to do).

As you can see, I've in total 3 cursors (1 for the groups and 2 for the children). The data is synced from a local database and put in a Content Provider. (The database is from here and the provider from here). From there I insert it into the CursorTreeAdapter. (The most of the loader functions is from here).

The only thing is now how do I implement a search? Should I do it trough Content Provider or a raw query in the database? I would like that the results of both children tables is displayed. I think because it's easy to make a fault while typing that `tokenize=porter` is a option in my case.

I've tried this in MyListAdapter.java (with FilterQueryProvider as someone suggested on Stack Overflow):

public void filterList(CharSequence constraint) {
final Cursor oldCursor = getCursor();
setFilterQueryProvider(filterQueryProvider);
getFilter().filter(constraint, new FilterListener() {
public void onFilterComplete(int count) {
// assuming your activity manages the Cursor
// (which is a recommended way)
notifyDataSetChanged();
//	stopManagingCursor(oldCursor);
//	final Cursor newCursor = getCursor();
//	startManagingCursor(newCursor);
//	// safely close the oldCursor
if (oldCursor != null && !oldCursor.isClosed()) {
oldCursor.close();
}
}
});
}

private FilterQueryProvider filterQueryProvider = new FilterQueryProvider() {
public Cursor runQuery(CharSequence constraint) {
// assuming you have your custom DBHelper instance
// ready to execute the DB request
String s = '%' + constraint.toString() + '%';
return mActivity.getContentResolver().query(CfpContract.Classes.CONTENT_URI,
MyListAdapter.CLASSES_PROJECTION,
CfpContract.Classes.CLASS_NAME + " LIKE ? OR " + CfpContract.Classes.CLASS_CAT + " LIKE ?",
new String[] { s, s },
null);
}
};

And this in MainActivity.java:

search.setOnQueryTextListener(new OnQueryTextListener() {

@Override
public boolean onQueryTextSubmit(String query) {
listAdapter.filterList(query);
expandAll();
return false;
}

@Override
public boolean onQueryTextChange(String query) {
listAdapter.filterList(query);
expandAll();
return false;
}
});

search.setOnCloseListener(new OnCloseListener() {

@Override
public boolean onClose() {
listAdapter.filterList("");
expandAll();
return false;
}
});

But then I get these errors when I try to search:

12-20 13:20:19.449: E/CursorWindow(28747): Failed to read row 0, column -1 from a CursorWindow which has 96 rows, 4 columns.
12-20 13:20:19.449: D/AndroidRuntime(28747): Shutting down VM
12-20 13:20:19.449: W/dalvikvm(28747): threadid=1: thread exiting with uncaught exception (group=0x415c62a0)
12-20 13:20:19.499: E/AndroidRuntime(28747): FATAL EXCEPTION: main
12-20 13:20:19.499: E/AndroidRuntime(28747): java.lang.IllegalStateException: Couldn't read row 0, col -1 from CursorWindow. Make sure the Cursor is initialized correctly before accessing data from it.

What I'm doing wrong? Or is this because I'm only return 1 query (Classes) instead of 2 (Classes and Teachers) in runQuery?

I hope that someone can point me in a good direction.