Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Error when using CursorLoader to load data from ContentProvider

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 ?

like image 598
user3168815 Avatar asked Jan 07 '14 12:01

user3168815


People also ask

How to get data from content provider in Android?

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.

How to access content 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.

What do you mean by content provider in Android?

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.


1 Answers

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!
    }
}
like image 162
AngraX Avatar answered Oct 29 '22 18:10

AngraX