Reputation: 1141
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).
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:
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:
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
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)
Upvotes: 3
Views: 2828
Reputation: 842
If you are using a Loader
to manage your Adapter
's Cursor
then you need to avoid adapter.filter()
usage at all cost. Since the filter()
within adapter expects a Cursor
in return which is impossible since loaders are async and runs in a background thread.
How to Replace adapter.filter()
with Loaders:
Within onSelect/Item/TextInput listener, store the selected/input values in a class variable.
Replace all calls to myAdapter.filter()
with myLoadManager.restartLoader(...)
.
In your onCreateLoader()
method, using the stored selected/inputted values you now have because of step 1, dynamically generate your sql/cursor query and run it.
This is how the sequence of events will play out:
User selects a spinner drop down and selects "USA".
You store "USA" in a class variable and then call myLoadManager.restartLoader(...)
.
Load manager destroys the previous load manager and create a new one calling onCreateLoader()
which has your auto-generate query code.
New loader runs the updated query and notifies your adapter to refresh & redraw it's data.
Why do you want to use Loaders
?
Free/easier async management of non-blocking UI updates based on slow data. Much easier than AsyncTasks
.
Free memory management of your cursors. Will auto clean/close Cursors
during re-query or activity close.
Upvotes: 0
Reputation: 1141
I found the solution (a solution) to my problem came in two parts:
Note: For anyone following, I'm still getting a bit of an error while running this code, but it's not fatal and it doesn't seem to be related to cursors, so I'm not addressing it here.
The big complication is that the runQueryOnBackgroundThread
method in the CursorAdapter requires that a cursor be returned. When using LoaderManager's, you don't get your hands on a cursor until an asynchronous callback, and that has downsides:
runQueryOnBackgroundThread
method.runQueryOnBackgroundThread
is the first method called with the new filtering constraint.runQueryOnBackgroundThread
method hands its cursor off to the changeCursor
method, which closes the cursors that are changed out (something we don't do when working with a LoaderManager/CursorLoader), so we don't want to follow that workflow anyway.By default, the runQueryOnBackgroundThread
method of the CursorAdapter just calls the runQuery
method of the CursorAdapter's FilterQueryProvider if it has been defined. I opted to define the FilterQueryProvider rather than override the runQueryOnBackgroundThread
method for a few reasons:
Note: The runQuery
method still requires that a cursor be returned, so we don't get to skirt that issue.
I decided to generate a dummy cursor in my FilterQueryProvider's runQuery
method. Then, since that dummy cursor would be handed off to the CursorAdapter's changeCursor
method, I overrode changeCursor
to simply close every cursor it was passed.
The runQuery
method also initiates the asynchronous LoaderManager call that includes the filtering constraint. Then the LoaderManager callbacks take care of swapping in the new cursors that are generated.
Note: Ideally, I suppose you could override the function that calls runQueryOnBackgroundThread
, and have it make the asynchronous LoaderManager call, but I couldn't figure out what that was.
I was trying to distinguish between unfiltered and filtered cursors to the unfiltered cursor could be used when the filtering constraint was null. After reading Android 3.0 - what are the advantages of using LoaderManager instances exactly? for the umpteenth time, I realized that the accepted answer was using the same CursorLoader to generate all cursors.
Instead of trying to hang on to the original unfiltered cursor, I decided I'd just generate a new unfiltered cursor whenever I needed it. The onCreateLoader
LoaderManager callback became a bit more complex (but more like the examples I had been seeing), and the onLoadFinished
callback became much more simple (like the examples I had been seeing).
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;
/**
* 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);
final LoaderManager.LoaderCallbacks<Cursor> iFace = this;
adapter.setFilterQueryProvider(new FilterQueryProvider() {
public Cursor runQuery(CharSequence constraint) {
if (constraint != null) {
Bundle bundle = new Bundle();
bundle.putCharSequence("constraint", constraint);
getLoaderManager().restartLoader(Home.LOADER_CONTACTS_CURSOR, bundle, iFace);
} else {
getLoaderManager().restartLoader(Home.LOADER_CONTACTS_CURSOR, null, iFace);
}
return getContentResolver().query(ContactsContract.Contacts.CONTENT_URI, null, null, null, null);
}
});
// Attach CursorAdapter to AutoCompleteTextView field
AutoCompleteTextView field = (AutoCompleteTextView) findViewById(R.id.newPlayer_edit);
field.setAdapter(adapter);
// Initialize Cursor using LoaderManagers
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");
Uri baseUri;
if (args != null) {
CharSequence constraint = args.getCharSequence("constraint");
if (DEBUG) Log.i(TAG, "Constraint: " + constraint.toString());
baseUri = Uri.withAppendedPath(ContactsContract.Contacts.CONTENT_FILTER_URI, Uri.encode(constraint.toString()));
} else {
if (DEBUG) Log.i(TAG, "No Constraint");
baseUri = ContactsContract.Contacts.CONTENT_URI;
}
return new CursorLoader(this, baseUri, null, null, null, null);
}
@Override
public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) {
if (DEBUG) Log.i(TAG, "Loader Callback: load finished");
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;
/**
* Constructor
*/
public AdapterContacts(Context context, Cursor c, int flags) {
super(context, c, flags);
}
/**
* 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) {
newCursor.close();
}
}
Upvotes: 6