Situation
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).
Notes
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 (http://developer.android.com/guide/topics/fundamentals/loaders.html#callback), but that doesn't seem to be the case.
Because the CursorAdapter wants me to return the original, unfiltered cursor when the filtering constraint is too small to use, I am:
- 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).
- 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.
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.
If I configure my Activity to manage orientation changes itself by adding the following to the activity
declaration in my manifest:
android:configChanges="orientation|screenSize"
the saved original cursor should be preserved across orientation changes (right?). While the app does not crash, another related problem occurs:
- 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.
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
My Questions
- Am I right in assuming my cursor is being destroyed when device orientation changes?
- How can a preserve a cursor and/or its data when device orientation changes?
Source Code
View - home.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical" >
<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" >
<requestFocus />
</AutoCompleteTextView>
</LinearLayout>
Activity - Home.java
public class Home extends Activity implements LoaderManager.LoaderCallbacks<Cursor> {
// 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<Cursor> 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<Cursor> 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<Cursor> loader) {
if (DEBUG) Log.i(TAG, "Loader Callback: resetting");
adapter.swapCursor(null);
}
}
CursorAdapter - AdapterContacts.java
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;
}
}
Error Message
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
Warning Message - when Activity is configured to manage orientation changes itself
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)