Я делаю приложение для  android и Я использую CursorTreeAdapter как ExpandableListView. Теперь я хочу использовать окно поиска для отображения фильтрованных элементов ExpandableListView. Например:
 android и Я использую CursorTreeAdapter как ExpandableListView. Теперь я хочу использовать окно поиска для отображения фильтрованных элементов ExpandableListView. Например: 
Вот код, который я до сих пор:
  MainActivity.java
package com.example.cursortreeadaptersearch;
import java.util.HashMap;
import android.app.SearchManager;
import android.content.Context;
import android.database.ContentObserver;
import android.database.Cursor;
import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
import android.provider.ContactsContract;
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;
public class MainActivity extends SherlockFragmentActivity {
    private SearchView search;
    private MyListAdapter listAdapter;
    private ExpandableListView myList;
    private final String DEBUG_TAG = getClass().getSimpleName().toString();
    /**
     * The columns we are interested in from the database
     */
    static final String[] CONTACTS_PROJECTION = new String[] {
            ContactsContract.Contacts._ID,
            ContactsContract.Contacts.DISPLAY_NAME,
            ContactsContract.Contacts.PHOTO_ID,
            ContactsContract.CommonDataKinds.Email.DATA,
            ContactsContract.CommonDataKinds.Photo.CONTACT_ID };
    static final String[] GROUPS_SUMMARY_PROJECTION = new String[] {
            ContactsContract.Groups.TITLE, ContactsContract.Groups._ID,
            ContactsContract.Groups.SUMMARY_COUNT,
            ContactsContract.Groups.ACCOUNT_NAME,
            ContactsContract.Groups.ACCOUNT_TYPE,
            ContactsContract.Groups.DATA_SET };
    @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.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;
            }
        });
        // 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(
                ContactsContract.Data.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;
            HashMap<Integer, Integer> groupMap = listAdapter.getGroupMap();
            if (id != -1) {
                int groupPos = groupMap.get(id);
                if (groupPos == 0) { // E-mail group
                    String[] PROJECTION = new String[] {
                            ContactsContract.RawContacts._ID,
                            ContactsContract.CommonDataKinds.Email.DATA };
                    String sortOrder = "CASE WHEN "
                            + ContactsContract.Contacts.DISPLAY_NAME
                            + " NOT LIKE '%@%' THEN 1 ELSE 2 END, "
                            + ContactsContract.Contacts.DISPLAY_NAME + ", "
                            + ContactsContract.CommonDataKinds.Email.DATA
                            + " COLLATE NOCASE";
                    String selection = ContactsContract.CommonDataKinds.Email.DATA
                            + " NOT LIKE ''";
                    cl = new CursorLoader(getApplicationContext(),
                            ContactsContract.CommonDataKinds.Email.CONTENT_URI,
                            PROJECTION, selection, null, sortOrder);
                } else if (groupPos == 1) { // Name group
                    Uri contactsUri = ContactsContract.Data.CONTENT_URI;
                    String selection = "(("
                            + ContactsContract.CommonDataKinds.GroupMembership.DISPLAY_NAME
                            + " NOTNULL) AND ("
                            + ContactsContract.CommonDataKinds.GroupMembership.HAS_PHONE_NUMBER
                            + "=1) AND ("
                            + ContactsContract.CommonDataKinds.GroupMembership.DISPLAY_NAME
                            + " != '') AND ("
                            + ContactsContract.CommonDataKinds.GroupMembership.GROUP_ROW_ID
                            + " = '1' ))"; // Row ID 1 == All contacts
                    String sortOrder = ContactsContract.CommonDataKinds.GroupMembership.DISPLAY_NAME
                            + " COLLATE LOCALIZED ASC";
                    cl = new CursorLoader(getApplicationContext(), contactsUri,
                            CONTACTS_PROJECTION, selection, null, sortOrder);
                }
            } else {
                // group cursor
                Uri groupsUri = ContactsContract.Groups.CONTENT_SUMMARY_URI;
                String selection = "((" + ContactsContract.Groups.TITLE
                        + " NOTNULL) AND (" + ContactsContract.Groups.TITLE
                        + " == 'Coworkers' ) OR ("
                        + ContactsContract.Groups.TITLE
                        + " == 'My Contacts' ))"; // Select only Coworkers
                                                 // (E-mail only) and My
                                                // Contacts (Name only)
                String sortOrder = ContactsContract.Groups.TITLE
                        + " COLLATE LOCALIZED ASC";
                cl = new CursorLoader(getApplicationContext(), groupsUri,
                        GROUPS_SUMMARY_PROJECTION, selection, null, sortOrder);
            }
            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.cursortreeadaptersearch;
import java.util.HashMap;
import android.content.Context;
import android.database.Cursor;
import android.provider.ContactsContract;
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.TextView;
public class MyListAdapter extends CursorTreeAdapter {
    public HashMap<String, View> childView = new HashMap<String, View>();
    /**
     * The columns we are interested in from the database
     */
    private final String DEBUG_TAG = getClass().getSimpleName().toString();
    protected final HashMap<Integer, Integer> mGroupMap;
    private MainActivity mActivity;
    private LayoutInflater mInflater;
    String mConstraint;
    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(ContactsContract.Groups.TITLE)));
        }
    }
    @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);
        if (txtListChild != null) {
            txtListChild.setText(cursor.getString(1)); // Selects E-mail or
                                                        // Display Name
        }
    }
    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(ContactsContract.Groups._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 filterList(CharSequence constraint) {
        // TODO Filter the data here
    }
}
Я очень значительно упростил и очистил код (так что вам, ребята, этого не нужно делать).
Как вы можете видеть, у меня всего 3 курсора (1 для групп и 2 для детей). Данные получаются из ContactsContract (которые являются контактами пользователя). Курсор от дочернего 1 представляет все электронные письма всех контактов, а курсор из дочернего 2 представляет все отображаемые имена контактов. (Большинство функций загрузчика от здесь).
Единственное, что мне теперь делать, как реализовать поиск? Должен ли я делать это через Content Provider или необработанный запрос в базе данных? Я бы хотел, чтобы отображались результаты обеих таблиц с детьми. Я думаю, потому что легко сделать ошибку, набрав tokenize=porter вариант в моем случае.
Я надеюсь, что кто-то может указать мне в хорошем направлении.
Edit:
Я пробовал это в  MyListAdapter.java (с FilterQueryProvider, как это было предложено Кайл I.):
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(ContactsContract.Data.CONTENT_URI,
                MainActivity.CONTACTS_PROJECTION,
                ContactsContract.CommonDataKinds.GroupMembership.DISPLAY_NAME + " LIKE ?",
            new String[] { s },
            null);
    }
};
И это в  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;
            }
        });
Но затем я получаю эти ошибки при попытке поиска:
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.
Что я делаю неправильно? Или это потому, что я только возвращаю 1 запрос (отображаемые имена) вместо 2 (отображаемые имена и сообщения электронной почты) в runQuery?
Изменить 2:
Прежде всего, я изменил все свои реализации базы данных на ContactsContract. Это становится проще в обслуживании, так что вам не нужно писать собственную реализацию базы данных.
Теперь я попытался сохранить мое ограничение в runQuery() FilterQueryProvider, а затем в getChildrenCursor запустить запрос к этому ограничению. (как предложено JRaymond)
private String mConstraint;
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(ContactsContract.Groups._ID));
    Log.d(DEBUG_TAG, "getChildrenCursor() for groupPos " + groupPos);
    Log.d(DEBUG_TAG, "getChildrenCursor() for groupId " + groupId);
    mGroupMap.put(groupId, groupPos);
    Bundle b = new Bundle();
    b.putString("constraint", mConstraint);
    Loader loader = mActivity.getSupportLoaderManager().getLoader(groupId);
    if (loader != null && !loader.isReset()) {
        if (mConstraint == null || mConstraint.isEmpty()) {
            // Normal query
            mActivity.getSupportLoaderManager().restartLoader(groupId,
                    null, mActivity.mSpeakersLoaderCallback);
        } else {
            // Constrained query
            mActivity.getSupportLoaderManager().restartLoader(groupId, b,
                    mActivity.mSpeakersLoaderCallback);
        }
    } else {
        if (mConstraint == null || mConstraint.isEmpty()) {
            // Normal query
            mActivity.getSupportLoaderManager().initLoader(groupId, null,
                    mActivity.mSpeakersLoaderCallback);
        } else {
            // Constrained query
            mActivity.getSupportLoaderManager().initLoader(groupId, b,
                    mActivity.mSpeakersLoaderCallback);
        }
    }
    return null;
}
И вот FilterQueryProvider:
private FilterQueryProvider filterQueryProvider = new FilterQueryProvider() {
    public Cursor runQuery(CharSequence constraint) {
        // Load the group cursor here and assign mConstraint
        mConstraint = constraint.toString();
        Uri groupsUri = ContactsContract.Groups.CONTENT_SUMMARY_URI;
        String selection = "((" + ContactsContract.Groups.TITLE
                + " NOTNULL) AND (" + ContactsContract.Groups.TITLE
                + " == 'Coworkers' ) OR (" + ContactsContract.Groups.TITLE
                + " == 'My Contacts' ))"; // Select only Coworkers
                                            // (E-mail only) and My
                                            // Contacts (Name only)
        String sortOrder = ContactsContract.Groups.TITLE
                + " COLLATE LOCALIZED ASC";
        return mActivity.getContentResolver().query(groupsUri,
                MainActivity.GROUPS_SUMMARY_PROJECTION, selection, null,
                sortOrder);
    }
};
Как вы можете видеть, я загружал запрос групп, чтобы получить работу getChildrenCursor. Только для запроса, который я должен выполнить в MainActivity, который я получаю из пакета?
Проект можно загрузить здесь, который вы можете импортировать в Eclipse.
