I'm building an Android app which uses OSM data to provide routes for users from a set of given locations. The user can type where they wish to go into a SearchView and as the user types the search results are filtered to narrow the results, they can then choose a destination from the drop down ListView. This filtering is done using onQueryTextChange(). I'm using a ContentProvider to query this data from a database and implementing the LoaderManager.LoaderCallbacks interface to requery the ContentProvider and provide new data for the adapter to use.
All of this works the vast, vast majority of the time and performs exactly as expected. However ,very rarely, the app will crash with the following stack trace.
java.lang.IllegalStateException: attempt to re-open an already-closed object: SQLiteQuery: SELECT _id, suggest_text_1, suggest_intent_data FROM Locations WHERE (suggest_text_1 LIKE ?)
at android.database.sqlite.SQLiteClosable.acquireReference(SQLiteClosable.java:55)
at android.database.sqlite.SQLiteQuery.fillWindow(SQLiteQuery.java:58)
at android.database.sqlite.SQLiteCursor.fillWindow(SQLiteCursor.java:152)
at android.database.sqlite.SQLiteCursor.onMove(SQLiteCursor.java:124)
at android.database.AbstractCursor.moveToPosition(AbstractCursor.java:214)
at android.database.CursorWrapper.moveToPosition(CursorWrapper.java:162)
at android.support.v4.widget.CursorAdapter.getItemId(CursorAdapter.java:225)
at android.widget.AdapterView.rememberSyncState(AdapterView.java:1195)
at android.widget.AdapterView$AdapterDataSetObserver.onChanged(AdapterView.java:811)
at android.widget.AbsListView$AdapterDataSetObserver.onChanged(AbsListView.java:6280)
at android.database.DataSetObservable.notifyChanged(DataSetObservable.java:37)
at android.widget.BaseAdapter.notifyDataSetChanged(BaseAdapter.java:50)
at android.support.v4.widget.CursorAdapter.swapCursor(CursorAdapter.java:347)
at android.support.v4.widget.SimpleCursorAdapter.swapCursor(SimpleCursorAdapter.java:326)
at android.support.v4.widget.CursorAdapter.changeCursor(CursorAdapter.java:315)
at android.support.v4.widget.CursorFilter.publishResults(CursorFilter.java:68)
at android.widget.Filter$ResultsHandler.handleMessage(Filter.java:282)
at android.os.Handler.dispatchMessage(Handler.java:102)
at android.os.Looper.loop(Looper.java:136)
at android.app.ActivityThread.main(ActivityThread.java:5017)
at java.lang.reflect.Method.invokeNative(Native Method)
at java.lang.reflect.Method.invoke(Method.java:515)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:779)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:595)
at dalvik.system.NativeStart.main(Native Method)
Usually this error occurs when I clear the SearchView to type in another search and other times it occurs as the user is typing.
Edit: Included a partial ContentProvider, omits insert, delete and update.
public class SearchContentProvider extends ContentProvider {
private DbHelper helper;
private static final String AUTH = "com.dgh1.Navigation.SearchContentProvider";
private static final String LOCATIONS_PATH = "Location";
private static final String GEOFENCES_PATH = "Fences";
private static final String PEOPLE_PATH = "People";
public static final Uri LOCATION_URI = Uri.parse("content://" + AUTH + "/" + LOCATIONS_PATH);
public static final Uri GEOFENCE_URI = Uri.parse("content://" + AUTH + "/" + GEOFENCES_PATH);
public static final Uri PEOPLE_URI = Uri.parse("content://" + AUTH + "/" + PEOPLE_PATH);
private static final int LOCATIONS = 10;
private static final int NAME_LOCATION = 11;
private static final int NODE_LOCATION = 20;
private static final int GEOFENCES = 30;
private static final int MARKERS = 40;
private static final int GEOFENCE = 50;
private static final int PEOPLE = 60;
private static final int PERSON = 70;
private static final String URI_ERROR = "Unknown URI: ";
private static final UriMatcher matcher = new UriMatcher(UriMatcher.NO_MATCH);
static {
matcher.addURI(AUTH, LOCATIONS_PATH, LOCATIONS);
matcher.addURI(AUTH, GEOFENCES_PATH, GEOFENCES);
matcher.addURI(AUTH, PEOPLE_PATH, PEOPLE);
matcher.addURI(AUTH, LOCATIONS_PATH + "/#", NODE_LOCATION);
matcher.addURI(AUTH, GEOFENCES_PATH + "/#", GEOFENCE);
matcher.addURI(AUTH, PEOPLE_PATH + "/#", PERSON);
matcher.addURI(AUTH, LOCATIONS_PATH + "/*", NAME_LOCATION);
}
@Override
public boolean onCreate() {
helper = new DbHelper(getContext());
return true;
}
@Override
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
Cursor cursor = null;
int uriType = matcher.match(uri);
switch (uriType) {
case LOCATIONS:
if ( selectionArgs == null ) {
cursor = helper.getAllLocations();
} else {
cursor = helper.getSuggestionsData(projection, selection, selectionArgs);
}
break;
case NODE_LOCATION:
cursor = helper.getSingleLocationById(uri.getLastPathSegment());
break;
case NAME_LOCATION:
cursor = helper.getSingleLocationByName(selectionArgs[0]);
break;
case MARKERS:
cursor = helper.getAllMarkers();
break;
case GEOFENCES:
cursor = helper.getAllFences();
break;
case PEOPLE:
if ( selectionArgs == null ) {
cursor = helper.getAllPeople();
} else {
cursor = helper.findPersonById(selectionArgs[0]);
}
break;
default:
Log.d(URI_ERROR, uri.toString());
}
cursor.setNotificationUri(getContext().getContentResolver(), uri);
return cursor;
}
.
.
.
}
Initialsing the loader and set the adapter:
public boolean onCreateOptionsMenu(Menu menu) {
// Inflate the menu; this adds items to the action bar if it is present.
getMenuInflater().inflate(R.menu.options_menu, menu);
SearchManager manager = (SearchManager) getSystemService(Context.SEARCH_SERVICE);
MenuItem item = menu.findItem(R.id.action_search);
SearchView view = (SearchView) MenuItemCompat.getActionView(item);
view.setIconified(false);
view.setSearchableInfo(
manager.getSearchableInfo(getComponentName()));
view.setSuggestionsAdapter(adapter);
view.setOnQueryTextListener(this);
getSupportLoaderManager().initLoader(1, null, this);
return true;
}
LoaderManager.LoaderCallbacks implementation:
@Override
public Loader<Cursor> onCreateLoader(int i, Bundle bundle) {
return new CursorLoader(this, SearchContentProvider.CONTENT_URI, new String[] { DbHelper.ID, DbHelper.LOCATION_NAME, DbHelper.LOCATION_NODE_ID },
DbHelper.LOCATION_NAME + " LIKE ?", new String[] { "%" + cursorFilter + "%"}, null );
}
@Override
public void onLoadFinished(Loader<Cursor> cursorLoader, Cursor cursor) {
if ( !(cursor.isClosed()) )
adapter.swapCursor(cursor);
}
}
@Override
public void onLoaderReset(Loader<Cursor> cursorLoader) {
adapter.swapCursor(null);
}
OnQueryTextchange():
@Override
public boolean onQueryTextChange(String s) {
cursorFilter = !TextUtils.isEmpty(s) ? s : null;
getSupportLoaderManager().restartLoader(0, null, this);
return true;
}
Declarations from DbHelper:
public static final String LOCATION_TABLE = "Locations";
public static final String LOCATION_NAME = SearchManager.SUGGEST_COLUMN_TEXT_1;
public static final String LOCATION_NODE_ID = SearchManager.SUGGEST_COLUMN_INTENT_DATA;
Declarations from onCreate():
adapter = new SimpleCursorAdapter(this, android.R.layout.simple_list_item_2, null, new String[] { DbHelper.LOCATION_NAME },
new int[] { android.R.id.text2 }, 0);
My question is has anyone experienced this before, and if so, have you found a solution ? Or am I simply wrong in my implementation ?
To retrieve data from a provider, your application needs "read access permission" for the provider. You can't request this permission at run-time; instead, you have to specify that you need this permission in your manifest, using the <uses-permission> element and the exact permission name defined by the provider.
To access the content, define a content provider URI address. Create a database to store the application data. Implement the six abstract methods of ContentProvider class. Register the content provider in AndroidManifest.
Content providers are one of the primary building blocks of Android applications, providing content to applications. They encapsulate data and provide it to applications through the single ContentResolver interface. A content provider is only required if you need to share data between multiple applications.
Here's what I did in the AutocompleteTextView for it to work with loaders. You basically disable the AutocompleteTextView's own filtering mechanism and use the loaders API instead. Example below is using an Activity. Adapt as necessary if using fragments.
String mCurrentFilter = "";
@Override
protected void onCreate(Bundle savedInstanceState) {
...
mACTextView = new NonFilterableAutoCompleteTextView(this);
// Start with a null cursor since data is not ready yet
mAdapter = new CursorAdapter(this, null, 0){...};
mACTextView.setAdapter(mAdapter);
mACTextView.addTextChangedListener(new TextWatcher() {
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
}
@Override
public void afterTextChanged(Editable s) {
mCurrentFilter = s.toString();
getLoaderManager().restartLoader(LOADER_SUGGESTIONS, null, ExampleActivity.this);
}
});
getLoaderManager().initLoader(LOADER_SUGGESTIONS, null, this);
}
@Override
public Loader<Cursor> onCreateLoader(int id, Bundle args) {
switch (id) {
case LOADER_SUGGESTIONS:
// Use the current filter to perform the query. For simplicity sake, assume an empty filter ("") will return all records.
return new CursorLoader(getActivity(), Uri.parse("Whatever your content provider URI is " + mCurrentFilter)), null, null, null, null);
...
}
return null;
}
@Override
public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
switch (loader.getId()) {
case LOADER_SUGGESTIONS:
mAdapter.swapCursor(data);
break;
...
}
}
/**
* An AutoCompleteTextView which does not perform any background filtering. This class will
* not perform any filtering and is intended to be used with CursorLoaders and CursorAdapters and
* have the cursor in the adapter swapped when the loader has new data.
* <p/>
* This is required since using the standard AutoCompleteTextView with CursorLoaders and swapCursor
* causes races conditions with the widget's own filtering happening in the background. The default filtering mechanism
* will run on a background thread with an instance of the old cursor.
*
* @author AngraX
*/
public static class NonFilterableAutoCompleteTextView extends AutoCompleteTextView {
public NonFilterableAutoCompleteTextView(Context context) {
super(context);
}
public NonFilterableAutoCompleteTextView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public NonFilterableAutoCompleteTextView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
@Override
protected void performFiltering(CharSequence text, int keyCode) {
// I say NO!
}
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With