• After 15+ years, we've made a big change: Android Forums is now Early Bird Club. Learn more here.

Apps CursorWrapperInner - Unable to close database

SFLeBrun

Lurker
My application has a ContentProvider that handles the direct SQLiteDatabase access. The activities that query the ContentProvider are returned a Cursor. Even though the activities close the cursor, the application is throwing an IllegalStateException when the ContentProvider exits (or possibly when garbage collection is done) because the activities are not closing the SQLiteDatabase.

The Activity has no direct way to close database. The Cursor returned is an android.content.ContentResolver$CursorWrapperInner type. This type encapsulates the actual SQLiteCursor returned from the ContentProvider.

If the returned Cursor could be cast into its original SQLiteCursor, the SQLiteDatabase used by the Cursor would be accessible and could be closed by the Activity. Unfortunately, the CursorWrapperInner cannot be cast.

This looks like it should be a common problem but I cannot find any references to this issue in any of the forums that I have looked at or by googling. Any help resolving this issue will be appreciated.

Sequence of Events:

1) Activity uses ContentResolver to run a query through a ContentProvider.

2) Content Provider receives the query request through a call to its query() method.

3) Content Provider opens a SQLiteDatabase, performs the query and obtains a SQLiteCursor.

4) Content Provider exits the query() method, returning the SQLiteCursor.

5) Activity receives a CursorWrapperInner object from the ContentResolver.query() call.

6) Activity uses the cursor and invokes the Cursor.close() method.At some later time, either the ContentProvider is deleted or garbage collection occurs. (I am not sure which is the trigger to the Exception)

7) An IllegalStateException is thrown because a SQLiteDatabase remains open and is a leak.

* Closing the SQLiteDatabase in the ContentProvider invalidates the Cursor before the Activity has a chance to use.

* Invoking close() on the Cursor, which is suppose to release all resources held by the Cursor, is not closing the SQLiteDatabase.

* The CursorWrapperInner class prevents the Activity from direct access to the SQLiteCursor which could be used to close the database.


What am I missing?

The following is a snippet from the LogCat:
Code:
D/dalvikvm(  722): GC freed 3058 objects / 180664 bytes in 143ms
E/Database(  722): Leak found
E/Database(  722): java.lang.IllegalStateException: /data/data/com.lebruns.android.BookManager/databases/BMMasterCatalog.db SQLiteDatabase created and never closed
E/Database(  722):     at android.database.sqlite.SQLiteDatabase.<init>(SQLiteDatabase.java:1580)
E/Database(  722):     at android.database.sqlite.SQLiteDatabase.openDatabase(SQLiteDatabase.java:638)
E/Database(  722):     at android.database.sqlite.SQLiteDatabase.openOrCreateDatabase(SQLiteDatabase.java:659)
E/Database(  722):     at android.database.sqlite.SQLiteDatabase.openOrCreateDatabase(SQLiteDatabase.java:652)
E/Database(  722):     at android.app.ApplicationContext.openOrCreateDatabase(ApplicationContext.java:463)
E/Database(  722):     at android.content.ContextWrapper.openOrCreateDatabase(ContextWrapper.java:181)
E/Database(  722):     at android.database.sqlite.SQLiteOpenHelper.getWritableDatabase(SQLiteOpenHelper.java:98)
E/Database(  722):     at android.database.sqlite.SQLiteOpenHelper.getReadableDatabase(SQLiteOpenHelper.java:158)
E/Database(  722):     at com.lebruns.android.BookManager.BookCaseProvider.QueryMasterCatalog(BookCaseProvider.java:714)
E/Database(  722):     at com.lebruns.android.BookManager.BookCaseProvider.query(BookCaseProvider.java:273)
E/Database(  722):     at android.content.ContentProvider$Transport.query(ContentProvider.java:129)
E/Database(  722):     at android.content.ContentResolver.query(ContentResolver.java:149)
E/Database(  722):     at com.lebruns.android.BookManager.Catalog.Refresh(Catalog.java:124)
E/Database(  722):     at com.lebruns.android.BookManager.MainActivity.onStart(MainActivity.java:52)
E/Database(  722):     at android.app.Instrumentation.callActivityOnStart(Instrumentation.java:1205)
E/Database(  722):     at android.app.Activity.performStart(Activity.java:3490)
E/Database(  722):     at android.app.Activity.performRestart(Activity.java:3518)
E/Database(  722):     at android.app.Activity.performResume(Activity.java:3523)
E/Database(  722):     at android.app.ActivityThread.performResumeActivity(ActivityThread.java:2619)
E/Database(  722):     at android.app.ActivityThread.handleResumeActivity(ActivityThread.java:2647)
E/Database(  722):     at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1717)
E/Database(  722):     at android.os.Handler.dispatchMessage(Handler.java:99)
E/Database(  722):     at android.os.Looper.loop(Looper.java:123)
E/Database(  722):     at android.app.ActivityThread.main(ActivityThread.java:3948)
E/Database(  722):     at java.lang.reflect.Method.invokeNative(Native Method)
E/Database(  722):     at java.lang.reflect.Method.invoke(Method.java:521)
E/Database(  722):     at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:782)
E/Database(  722):     at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:540)
E/Database(  722):     at dalvik.system.NativeStart.main(Native Method)
 
While I did not find the answer to my original question, I did create a work around that appears to solve my Cursor/Database leak issue. The problem that I am seeing appears to be a general one but there is almost nothing written anywhere that I have been able to find.

The problem:
ContentProvider leak Cursor.

The result is that you need to leave the database open when the ContentProvider returns a Cursor object during a query() call. When garbage collection occurs, it finds a database that has not been closed and throws an InvalidStateException. When the Activity invokes the close() method on the cursor it is provided, the database does not get closed. Closing the database in the ContentProvider results in the cursor containing no data. The SQLiteCursor could close the databse through its SQLiteCursor.getDatabase() call but the cursor returned from the ContentProvider is wrapped in a class that does not provide access to the actual cursor object.

There are two solutions that I have discovered. The first is to make sure that the ContentProvider hangs on to every database that it opens for a query() call. If your ContentProvider only deals with a single database, this is an easy solution to implement using a private data member in your ContentProvider.

My ContentProvider deals with multiple databases, most of which have the same schema but contain different data. My first attempt was to create a container object and placed each database from a query() into it and then closing the database in the finalize(). This works but is not a great solution since each query() call can result in another database object being created. So I opted for a different solution. Use a Cursor that closes the database when the Cursor itself is closed.

Basically, I extended the SQLiteCursor class, overriding the close() method and adding a closeForReuse() method that allows the cursor to be closed without closing the attached database. The Activity that invokes the ContentProvider query method is responsible for insuring the cursor object it receives gets closed.

Only one book out of about half a dozen even mentioned that there was a problem here. That book is "Unlocking Android" from Manning.

The following code is a sample of my solution, with only the relative parts being present.

Code:
public class LeaklessProvider extends android.content.ContentProvider
{

    // Used for debugging, to insure that every cursor created
    // is closed.  Tracked through LogCat.
    static private int CursorID = 0;


    //=================================================
     // Nested class that extends SQLiteOpenHelper used for
    // opening and creating databases.
    public class LeaklessDatabase extends SQLiteOpenHelper
    {
        public LeaklessDatabase (Context context,
                                 String  databaseName,
                                 String  databaseFileName,
                                 int     dbVersion)
        {
            super(context,
                  databaseFileName,
                  new LeaklessCursorFactory(),
                  dbVersion);
        }

        // Fill in rest of class...
    }   // end of LeaklessDatabase class


    //=================================================
    // Nested Class that are LeaklessCursor for queries
    public class LeaklessCursor extends SQLiteCursor
    {
        static final String LogTag =
            "BookManager.LeaklessProvider.LeaklessCursor";

        final  SQLiteDatabase mDatabase;
        final  int            mID;


        // CTor - same signature as the SQLiteCursor with an extra parameter
        //        that is used for debugging/tracking
        public LeaklessCursor(SQLiteDatabase      database,
                              SQLiteCursorDriver  driver,
                              String              table,
                              SQLiteQuery         query,
                              int                 cursorID)
        {
            super(database, driver, table, query);

            mDatabase = database;
            mID       = cursorID;
        }

        /**
         * Closes the database used to generate the cursor when the
         * cursor is closed.  Hopefully, plugging the GC Leak detected
         * when using pure SQLiteCursor that are wrapped when returned
         * to an Activity and therefore unreachable.
         */
        @Override
        public void close()
        {
            Log.d(".close()", "Closing LeaklessCursor #" + mID
                     + " and database. " + mDatabase.getPath());
            super.close();
            if ( mDatabase != null )
            {
                mDatabase.close();
            }
        }

        /**
         * Closes cursor without closing database.
         */
        public void closeForReuse()
        {
            Log.d(".close()", "Closing LeaklessCursor #" + mID
                     + " but not database. " + mDatabase.getPath());
            super.close();
        }

        /**
         * Override toString() to add the ID value to the output.
         */
        @Override
        public String toString()
        {
            return super.toString() + ", ID# " + mID;
        }

    }   // end of LeaklessCursor class

    //=================================================
    // Nested Class to create the LeaklessCursor for queries

    class LeaklessCursorFactory implements SQLiteDatabase.CursorFactory
    {
        /**
         * Creates and returns a new Cursor of LeaklessCursor type.
         */
        public Cursor newCursor ( SQLiteDatabase      database,
                                  SQLiteCursorDriver  driver,
                                  String              editTable,
                                  SQLiteQuery         query )
        {
            int  cursorID = LeaklessProvider.CursorID++;
            Log.d(".LeaklessCursorFactory.newCursor()",
                    "Creating new Cursor.  ID: " + cursorID
                    + ", Database: " + database.getPath());

            return new LeaklessCursor(database,
                                      driver,
                                      editTable,
                                      query,
                                      cursorID);
        }

    }   // end of LeaklessCursorFactory class

}  // end of class LeaklessProvider
 
hi, thanks for the good post
i am new to java
how do i implement the needed overridden functions in the contentprovider class that you created for example:

@Override
public boolean onCreate() {
// TODO Auto-generated method stub
return false;
}

@Override
public Cursor query(Uri uri, String[] projection, String selection,
String[] selectionArgs, String sortOrder) {
// TODO Auto-generated method stub
return null;
}


thanks in advance
 
SFLeBrun:

Thank you for saving my ass. I inherited an android app whose DAL is built like your original -- leaky cursors and all. I'm implementing your solution as we speak.
 
Back
Top Bottom