title: CursorTreeAdapter with search implementation
body: <p>I'm making an application for <a href="/questions/tagged/android" class="post-tag" title="show questions tagged &#39;android&#39;" rel="tag">android</a> and I'm using CursorTreeAdapter as ExpandableListView. I want to use a search box for displaying the filtered ExpandableListView items. Like this:</p>
  <p><a href="https://i.stack.imgur.com/dieyY.png" rel="nofollow noreferrer"><img src="https://i.stack.imgur.com/dieyY.png" alt="http://i.imgur.com/8ua7Mkl.png" /></a></p>
  <p>Here's the code what I've written so far:</p>
  <p><strong><code>MainActivity.java</code></strong>:</p>
  <pre><code>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(&quot;&quot;);
                  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&lt;Cursor&gt; loader = getSupportLoaderManager().getLoader(-1);
          if (loader != null &amp;&amp; !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 &lt; count; i++) {
              myList.expandGroup(i);
          }
      }
  
      public LoaderManager.LoaderCallbacks&lt;Cursor&gt; mSpeakersLoaderCallback = new LoaderCallbacks&lt;Cursor&gt;() {
  
          @Override
          public Loader&lt;Cursor&gt; onCreateLoader(int id, Bundle args) {
              Log.d(DEBUG_TAG, &quot;onCreateLoader for loader_id &quot; + id);
              CursorLoader cl = null;
  
              HashMap&lt;Integer, Integer&gt; 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 = &quot;CASE WHEN &quot;
                              + ContactsContract.Contacts.DISPLAY_NAME
                              + &quot; NOT LIKE '%@%' THEN 1 ELSE 2 END, &quot;
                              + ContactsContract.Contacts.DISPLAY_NAME + &quot;, &quot;
                              + ContactsContract.CommonDataKinds.Email.DATA
                              + &quot; COLLATE NOCASE&quot;;
                      String selection = ContactsContract.CommonDataKinds.Email.DATA
                              + &quot; NOT LIKE ''&quot;;
                      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 = &quot;((&quot;
                              + ContactsContract.CommonDataKinds.GroupMembership.DISPLAY_NAME
                              + &quot; NOTNULL) AND (&quot;
                              + ContactsContract.CommonDataKinds.GroupMembership.HAS_PHONE_NUMBER
                              + &quot;=1) AND (&quot;
                              + ContactsContract.CommonDataKinds.GroupMembership.DISPLAY_NAME
                              + &quot; != '') AND (&quot;
                              + ContactsContract.CommonDataKinds.GroupMembership.GROUP_ROW_ID
                              + &quot; = '1' ))&quot;; // Row ID 1 == All contacts
                      String sortOrder = ContactsContract.CommonDataKinds.GroupMembership.DISPLAY_NAME
                              + &quot; COLLATE LOCALIZED ASC&quot;;
  
                      cl = new CursorLoader(getApplicationContext(), contactsUri,
                              CONTACTS_PROJECTION, selection, null, sortOrder);
                  }
              } else {
                  // group cursor
                  Uri groupsUri = ContactsContract.Groups.CONTENT_SUMMARY_URI;
                  String selection = &quot;((&quot; + ContactsContract.Groups.TITLE
                          + &quot; NOTNULL) AND (&quot; + ContactsContract.Groups.TITLE
                          + &quot; == 'Coworkers' ) OR (&quot;
                          + ContactsContract.Groups.TITLE
                          + &quot; == 'My Contacts' ))&quot;; // Select only Coworkers
                                                   // (E-mail only) and My
                                                  // Contacts (Name only)
                  String sortOrder = ContactsContract.Groups.TITLE
                          + &quot; COLLATE LOCALIZED ASC&quot;;
                  cl = new CursorLoader(getApplicationContext(), groupsUri,
                          GROUPS_SUMMARY_PROJECTION, selection, null, sortOrder);
              }
  
              return cl;
          }
  
          @Override
          public void onLoadFinished(Loader&lt;Cursor&gt; loader, Cursor data) {
              // Swap the new cursor in.
              int id = loader.getId();
  //          Log.d(&quot;Dump Cursor MainActivity&quot;,
  //                  DatabaseUtils.dumpCursorToString(data));
              Log.d(DEBUG_TAG, &quot;onLoadFinished() for loader_id &quot; + id);
              if (id != -1) {
                  // child cursor
                  if (!data.isClosed()) {
                      Log.d(DEBUG_TAG, &quot;data.getCount() &quot; + data.getCount());
  
                      HashMap&lt;Integer, Integer&gt; groupMap = listAdapter
                              .getGroupMap();
                      try {
                          int groupPos = groupMap.get(id);
                          Log.d(DEBUG_TAG, &quot;onLoadFinished() for groupPos &quot;
                                  + groupPos);
                          listAdapter.setChildrenCursor(groupPos, data);
                      } catch (NullPointerException e) {
                          Log.w(&quot;DEBUG&quot;,
                                  &quot;Adapter expired, try again on the next query: &quot;
                                          + e.getMessage());
                      }
                  }
              } else {
                  listAdapter.setGroupCursor(data);
              }
          }
  
          @Override
          public void onLoaderReset(Loader&lt;Cursor&gt; loader) {
              // This is called when the last Cursor provided to onLoadFinished()
              // is about to be closed.
              int id = loader.getId();
              Log.d(DEBUG_TAG, &quot;onLoaderReset() for loader_id &quot; + id);
              if (id != 1) {
                  // child cursor
                  try {
                      listAdapter.setChildrenCursor(id, null);
                  } catch (NullPointerException e) {
                      Log.w(DEBUG_TAG,
                              &quot;Adapter expired, try again on the next query: &quot;
                                      + 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);
                      }
                  });
              }
          }
      };
  }
  </code></pre>
  <p><strong><code>MyListAdapter.java</code></strong>:</p>
  <pre><code>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&lt;String, View&gt; childView = new HashMap&lt;String, View&gt;();
  
      /**
       * The columns we are interested in from the database
       */
  
      private final String DEBUG_TAG = getClass().getSimpleName().toString();
  
      protected final HashMap&lt;Integer, Integer&gt; 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&lt;Integer, Integer&gt;();
      }
  
      @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, &quot;getChildrenCursor() for groupPos &quot; + groupPos);
          Log.d(DEBUG_TAG, &quot;getChildrenCursor() for groupId &quot; + groupId);
  
          mGroupMap.put(groupId, groupPos);
  
          Loader loader = mActivity.getSupportLoaderManager().getLoader(groupId);
          if (loader != null &amp;&amp; !loader.isReset()) {
              mActivity.getSupportLoaderManager().restartLoader(groupId, null,
                      mActivity.mSpeakersLoaderCallback);
          } else {
              mActivity.getSupportLoaderManager().initLoader(groupId, null,
                      mActivity.mSpeakersLoaderCallback);
          }
  
          return null;
      }
  
      // Access method
      public HashMap&lt;Integer, Integer&gt; getGroupMap() {
          return mGroupMap;
      }
  
      public void filterList(CharSequence constraint) {
          // TODO Filter the data here
      }
  }
  </code></pre>
  <p>I have very considerably simplified and cleaned the code (so that you guys that not need to do).</p>
  <p>As you can see, I've in total 3 cursors (1 for the groups and 2 for the children). The data is get from <a href="http://developer.android.com/reference/android/provider/ContactsContract.html" rel="nofollow noreferrer">ContactsContract</a> (which are the contacts of the user).
  The cursor from child 1 represents all the e-mails of all contacts and the cursor from child 2 represents all the display names of the contacts. (The most of the loader functions is from <a href="https://stackoverflow.com/questions/10611927/simplecursortreeadapter-and-cursorloader-for-expandablelistview#answer-10989548">here</a>).</p>
  <p>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 <code>tokenize=porter</code> is a option in my case.</p>
  <p>I hope that someone can point me in a good direction.</p>
  <p><strong>Edit:</strong></p>
  <p>I've tried this in <strong><code>MyListAdapter.java</code></strong> (with <a href="http://developer.android.com/reference/android/widget/FilterQueryProvider.html" rel="nofollow noreferrer"><code>FilterQueryProvider</code></a> as suggested by <a href="https://stackoverflow.com/questions/20585273/cursortreeadapter-with-search-implementation#answer-20631443">Kyle I.</a>):</p>
  <pre><code>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 &amp;&amp; !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 + &quot; LIKE ?&quot;,
              new String[] { s },
              null);
      }
  };
  </code></pre>
  <p>And this in <strong><code>MainActivity.java</code></strong>:</p>
  <pre><code>        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(&quot;&quot;);
                  expandAll();
                  return false;
              }
          });
  </code></pre>
  <p>But then I get these errors when I try to search:</p>
  <pre><code>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.
  </code></pre>
  <p>What I'm doing wrong? Or is this because I'm only return 1 query (display names) instead of 2 (display names and e-mails) in <code>runQuery</code>?</p>
  <p><strong>Edit 2:</strong></p>
  <p>First of all I've changed all my database implementations to <a href="http://developer.android.com/reference/android/provider/ContactsContract.html" rel="nofollow noreferrer">ContactsContract</a>. This is become easier to maintain so that you don't have to write your own database implementation.</p>
  <p>What I now have tried is to save my constraint in <code>runQuery()</code> of <a href="http://developer.android.com/reference/android/widget/FilterQueryProvider.html" rel="nofollow noreferrer"><code>FilterQueryProvider</code></a>, and then in <code>getChildrenCursor</code> run a query against that constraint. (as suggested by <a href="https://stackoverflow.com/questions/20585273/cursortreeadapter-with-search-implementation#answer-21101892">JRaymond</a>)</p>
  <pre><code>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, &quot;getChildrenCursor() for groupPos &quot; + groupPos);
      Log.d(DEBUG_TAG, &quot;getChildrenCursor() for groupId &quot; + groupId);
  
      mGroupMap.put(groupId, groupPos);
  
      Bundle b = new Bundle();
      b.putString(&quot;constraint&quot;, mConstraint);
  
      Loader loader = mActivity.getSupportLoaderManager().getLoader(groupId);
      if (loader != null &amp;&amp; !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;
  }
  </code></pre>
  <p>And here is the <a href="http://developer.android.com/reference/android/widget/FilterQueryProvider.html" rel="nofollow noreferrer"><code>FilterQueryProvider</code></a>:</p>
  <pre><code>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 = &quot;((&quot; + ContactsContract.Groups.TITLE
                  + &quot; NOTNULL) AND (&quot; + ContactsContract.Groups.TITLE
                  + &quot; == 'Coworkers' ) OR (&quot; + ContactsContract.Groups.TITLE
                  + &quot; == 'My Contacts' ))&quot;; // Select only Coworkers
                                              // (E-mail only) and My
                                              // Contacts (Name only)
          String sortOrder = ContactsContract.Groups.TITLE
                  + &quot; COLLATE LOCALIZED ASC&quot;;
          return mActivity.getContentResolver().query(groupsUri,
                  MainActivity.GROUPS_SUMMARY_PROJECTION, selection, null,
                  sortOrder);
      }
  };
  </code></pre>
  <p>As you can see I've load the query of the groups in order to get the <code>getChildrenCursor</code> working. Only what for query should I run in <code>MainActivity</code> that I get from the bundle?</p>