Changing device orientation crashes Activity when using AutoCompleteTextView and CursorAdapter
<h1>Situation</h1>

<p>I have an Activity with an AutoCompleteTextView. As you type, the AutoCompleteTextView finds matching names from your contacts and displays them in a list. If the device orientation is changed while this list is displayed, the Activity crashes (Error message provided after source code).</p>

<h2>Notes</h2>

<p>I am developing for ICS 4.0.3 and testing on a Nexus S device. I am trying to follow best practices of using LoaderManager to generate and manage cursors. My understanding is that the LoaderManager should preserve cursor data across an orientation change (<a href="http://developer.android.com/guide/topics/fundamentals/loaders.html#callback" rel="nofollow">http://developer.android.com/guide/topics/fundamentals/loaders.html#callback</a>), but that doesn't seem to be the case.</p>

<p>Because the CursorAdapter wants me to return the original, unfiltered cursor when the filtering constraint is too small to use, I am:</p>

<ol>
<li>Saving the cursor as a static variable of the Activity for use when filtering criteria is not met (sorry for improper terminology. I'm a Java newbie).</li>
<li>Preventing the CursorAdapter from closing cursors after it replaces them, unless it confirms that it's not the original cursor by comparing it with the saved cursor.</li>
</ol>

<p>The problem appears to be that the onLoadFinished LoaderManager callback is being called after an orientation change, but the cursor it's passing (the original cursor?) was closed during the reorientation.</p>

<p>If I configure my Activity to manage orientation changes itself by adding the following to the <code>activity</code> declaration in my manifest:</p>

<blockquote>
  <p>android:configChanges="orientation|screenSize"</p>
</blockquote>

<p>the saved original cursor should be preserved across orientation changes (right?). While the app does not crash, another related problem occurs:</p>

<ul>
<li>If I type a few letter, change the device orientation, and then start deleting letters, once I get down to 1 or 0 letters, LogCat gives me a warning that I'm trying to access a cursor after it's been closed.</li>
</ul>

<p>It appears that my original cursor is gone in this case also. I'm guessing the app didn't crash because the onLoadFinished callback is not called when my activity is configured to manage orientation changes itself</p>

<h1>My Questions</h1>

<ol>
<li>Am I right in assuming my cursor is being destroyed when device orientation changes?</li>
<li>How can a preserve a cursor and/or its data when device orientation changes?</li>
</ol>

<h1>Source Code</h1>

<p>View - <code>home.xml</code></p>

<pre><code>&lt;?xml version="1.0" encoding="utf-8"?&gt;
&lt;LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical" &gt;

    &lt;AutoCompleteTextView
        android:id="@+id/newPlayer_edit"
        android:inputType="text"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_gravity="center_horizontal"
        android:hint="Contact"
        android:singleLine="true" &gt;
        &lt;requestFocus /&gt;
    &lt;/AutoCompleteTextView&gt;

&lt;/LinearLayout&gt;
</code></pre>

<p>Activity - <code>Home.java</code></p>

<pre><code>public class Home extends Activity implements LoaderManager.LoaderCallbacks&lt;Cursor&gt; {

// Constants
private static final String TAG = "HOME";
private static final boolean DEBUG = true;
public static final int LOADER_CONTACTS_CURSOR = 1;

// Variables
private AdapterContacts adapter;
public static Cursor originalCursor = null;


/**
 * Overrides
 */

@Override
public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);

    // Set the view
    setContentView(R.layout.home);

    // Initialize CursorAdapter
    adapter = new AdapterContacts(this, null, 0);

    // Attach CursorAdapter to AutoCompleteTextView field
    AutoCompleteTextView field = (AutoCompleteTextView) findViewById(R.id.newPlayer_edit);
    field.setAdapter(adapter);

    // Initialize Cursor using LoaderManager
    LoaderManager.enableDebugLogging(true);
    getLoaderManager().initLoader(LOADER_CONTACTS_CURSOR, null, this);
}

@Override
public void onDestroy() {
    if (DEBUG) Log.i(TAG, "Destroying activity");
    super.onDestroy();
}

@Override
public Loader&lt;Cursor&gt; onCreateLoader(int id, Bundle args) {
    if (DEBUG) Log.i(TAG, "Loader Callback: creating loader");
    return new CursorLoader(this, ContactsContract.Contacts.CONTENT_URI, null, null, null, null);
}

@Override
public void onLoadFinished(Loader&lt;Cursor&gt; loader, Cursor cursor) {
    if (DEBUG) Log.i(TAG, "Loader Callback: load finished");
    // If no cursor has been loaded before, reserve this cursor as the original
    // It will be returned by the CursorAdapter when the filter constraint is null 
    if (originalCursor == null) {
        originalCursor = cursor;
    }

    // add the cursor to the adapter
    adapter.swapCursor(cursor);
}

@Override
public void onLoaderReset(Loader&lt;Cursor&gt; loader) {
    if (DEBUG) Log.i(TAG, "Loader Callback: resetting");
    adapter.swapCursor(null);
}
}
</code></pre>

<p>CursorAdapter - <code>AdapterContacts.java</code></p>

<pre><code>public class AdapterContacts extends CursorAdapter {

// Constants
private static final String TAG = "AdapterContacts";
private static final boolean DEBUG = true;

// Variables
private TextView mName;
private ContentResolver mContent;

/**
 * Constructor
 */
public AdapterContacts(Context context, Cursor c, int flags) {
    super(context, c, flags);
    mContent = context.getContentResolver();
}

/**
 * Overrides
 */

@Override
public View newView(Context context, Cursor cursor, ViewGroup parent) {
    // Inflate the views that create each row of the dropdown list
    final LayoutInflater inflater = LayoutInflater.from(context);
    final LinearLayout ret = new LinearLayout(context);
    ret.setOrientation(LinearLayout.VERTICAL);

    mName = (TextView) inflater.inflate(android.R.layout.simple_dropdown_item_1line, parent, false);
    ret.addView(mName, new LinearLayout.LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT));

    /*
    int nameIdx = cursor.getColumnIndexOrThrow(ContactsContract.Contacts.DISPLAY_NAME_PRIMARY);
    mName.setText(cursor.getString(nameIdx));
    */

    return ret;
}

@Override
public void bindView(View view, Context context, Cursor cursor) {
    // Fill the dropdown row with data from the cursor
    int nameIdx = cursor.getColumnIndexOrThrow(ContactsContract.Contacts.DISPLAY_NAME_PRIMARY);
    String name = cursor.getString(nameIdx);
    ((TextView) ((LinearLayout) view).getChildAt(0)).setText(name);
}

@Override
public String convertToString(Cursor cursor) {
    // Convert the dropdown list entry that the user clicked on
    // into a string that will fill the AutoCompleteTextView
    int nameCol = cursor.getColumnIndexOrThrow(ContactsContract.Contacts.DISPLAY_NAME_PRIMARY);
    return cursor.getString(nameCol);
}

@Override
public void changeCursor(Cursor newCursor) {
    // Because a LoaderManager is used to initialize the originalCursor
    // changeCursor (which closes cursors be default when they're released)
    // is overridden to use swapCursor (which doesn't close cursors). 
    Cursor oldCursor = swapCursor(newCursor);

    // Any swapped out cursors that are not the original cursor must 
    // then be closed manually.
    if (oldCursor != Home.originalCursor) {
        oldCursor.close();
    }
}

@Override
public Cursor runQueryOnBackgroundThread(CharSequence constraint) {
    // If their is a constraint, generate and return a new cursor
    if (constraint != null) {
        // I'd love to use a LoaderManager here too,
        // but haven't quite figured out the best way.
        if (DEBUG) Log.i(TAG, "Constraint is not null: " + constraint.toString());
        Uri uri = Uri.withAppendedPath(ContactsContract.Contacts.CONTENT_FILTER_URI, constraint.toString());
        return mContent.query(uri, null, null, null, null);
    }

    // If no constraint, return the originalCursor
    if (DEBUG) Log.i(TAG, "Constraint is null");
    return Home.originalCursor;
}
}
</code></pre>

<p>Error Message</p>

<pre><code>03-16 10:39:34.839: E/AndroidRuntime(22097): java.lang.RuntimeException: Unable to start activity ComponentInfo{com.myapp.basic/com.myapp.basic.Home}: android.database.StaleDataException: Attempted to access a cursor after it has been closed.
03-16 10:39:34.839: E/AndroidRuntime(22097):    at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:1956)
03-16 10:39:34.839: E/AndroidRuntime(22097):    at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:1981)
03-16 10:39:34.839: E/AndroidRuntime(22097):    at android.app.ActivityThread.handleRelaunchActivity(ActivityThread.java:3351)
03-16 10:39:34.839: E/AndroidRuntime(22097):    at android.app.ActivityThread.access$700(ActivityThread.java:123)
03-16 10:39:34.839: E/AndroidRuntime(22097):    at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1151)
03-16 10:39:34.839: E/AndroidRuntime(22097):    at android.os.Handler.dispatchMessage(Handler.java:99)
03-16 10:39:34.839: E/AndroidRuntime(22097):    at android.os.Looper.loop(Looper.java:137)
03-16 10:39:34.839: E/AndroidRuntime(22097):    at android.app.ActivityThread.main(ActivityThread.java:4424)
03-16 10:39:34.839: E/AndroidRuntime(22097):    at java.lang.reflect.Method.invokeNative(Native Method)
03-16 10:39:34.839: E/AndroidRuntime(22097):    at java.lang.reflect.Method.invoke(Method.java:511)
03-16 10:39:34.839: E/AndroidRuntime(22097):    at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:784)
03-16 10:39:34.839: E/AndroidRuntime(22097):    at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:551)
03-16 10:39:34.839: E/AndroidRuntime(22097):    at dalvik.system.NativeStart.main(Native Method)
03-16 10:39:34.839: E/AndroidRuntime(22097): Caused by: android.database.StaleDataException: Attempted to access a cursor after it has been closed.
03-16 10:39:34.839: E/AndroidRuntime(22097):    at android.database.BulkCursorToCursorAdaptor.throwIfCursorIsClosed(BulkCursorToCursorAdaptor.java:75)
03-16 10:39:34.839: E/AndroidRuntime(22097):    at android.database.BulkCursorToCursorAdaptor.getColumnNames(BulkCursorToCursorAdaptor.java:170)
03-16 10:39:34.839: E/AndroidRuntime(22097):    at android.database.AbstractCursor.getColumnIndex(AbstractCursor.java:248)
03-16 10:39:34.839: E/AndroidRuntime(22097):    at android.database.AbstractCursor.getColumnIndexOrThrow(AbstractCursor.java:265)
03-16 10:39:34.839: E/AndroidRuntime(22097):    at android.database.CursorWrapper.getColumnIndexOrThrow(CursorWrapper.java:78)
03-16 10:39:34.839: E/AndroidRuntime(22097):    at android.widget.CursorAdapter.swapCursor(CursorAdapter.java:338)
03-16 10:39:34.839: E/AndroidRuntime(22097):    at com.myapp.basic.Home.onLoadFinished(Home.java:70)
03-16 10:39:34.839: E/AndroidRuntime(22097):    at com.myapp.basic.Home.onLoadFinished(Home.java:1)
03-16 10:39:34.839: E/AndroidRuntime(22097):    at android.app.LoaderManagerImpl$LoaderInfo.callOnLoadFinished(LoaderManager.java:438)
03-16 10:39:34.839: E/AndroidRuntime(22097):    at android.app.LoaderManagerImpl$LoaderInfo.finishRetain(LoaderManager.java:309)
03-16 10:39:34.839: E/AndroidRuntime(22097):    at android.app.LoaderManagerImpl.finishRetain(LoaderManager.java:765)
03-16 10:39:34.839: E/AndroidRuntime(22097):    at android.app.Activity.performStart(Activity.java:4485)
03-16 10:39:34.839: E/AndroidRuntime(22097):    at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:1929)
03-16 10:39:34.839: E/AndroidRuntime(22097):    ... 12 more
</code></pre>

<p>Warning Message - when Activity is configured to manage orientation changes itself</p>

<pre><code>03-16 10:47:50.804: W/Filter(22739): An exception occured during performFiltering()!
03-16 10:47:50.804: W/Filter(22739): android.database.StaleDataException: Attempted to access a cursor after it has been closed.
03-16 10:47:50.804: W/Filter(22739):    at android.database.BulkCursorToCursorAdaptor.throwIfCursorIsClosed(BulkCursorToCursorAdaptor.java:75)
03-16 10:47:50.804: W/Filter(22739):    at android.database.BulkCursorToCursorAdaptor.getCount(BulkCursorToCursorAdaptor.java:81)
03-16 10:47:50.804: W/Filter(22739):    at android.database.CursorWrapper.getCount(CursorWrapper.java:57)
03-16 10:47:50.804: W/Filter(22739):    at android.widget.CursorFilter.performFiltering(CursorFilter.java:53)
03-16 10:47:50.804: W/Filter(22739):    at android.widget.Filter$RequestHandler.handleMessage(Filter.java:234)
03-16 10:47:50.804: W/Filter(22739):    at android.os.Handler.dispatchMessage(Handler.java:99)
03-16 10:47:50.804: W/Filter(22739):    at android.os.Looper.loop(Looper.java:137)
03-16 10:47:50.804: W/Filter(22739):    at android.os.HandlerThread.run(HandlerThread.java:60)
</code></pre>
