Search code examples
androidsqlitelistviewsimplecursoradapterandroid-loadermanager

NullPointerException: uri - LoaderManager / SQLiteManager / ListView


I get this error when populating a ListView, inside of a Fragment using SQLiteCursorLoader and LoaderManager. If I remove the SQLiteCursorLoader and direct population from the adapter, it all works fine. If I try a custom adapter, I get the same error. The error seems to be that the mUri member of CursorLoader is never populated and left null, SQLiteCursorLoader never calls the constructor on CursorLoader that sets mUri.

public CursorLoader(Context context, Uri uri, String[] projection, String selection,
        String[] selectionArgs, String sortOrder) {
    super(context);
    mObserver = new ForceLoadContentObserver();
    mUri = uri;
    mProjection = projection;
    mSelection = selection;
    mSelectionArgs = selectionArgs;
    mSortOrder = sortOrder;
}

Instead it called the version of the constructor that only passes context.

public CursorLoader(Context context) {
    super(context);
    mObserver = new ForceLoadContentObserver();
}

Here is the error

03-08 15:00:36.906 2601-2683/com.overunitystudios.medbuddy E/AndroidRuntime: FATAL EXCEPTION: AsyncTask #1
                                                                             Process: com.overunitystudios.medbuddy, PID: 2601
                                                                             java.lang.RuntimeException: An error occurred while executing doInBackground()
                                                                                 at android.os.AsyncTask$3.done(AsyncTask.java:309)
                                                                                 at java.util.concurrent.FutureTask.finishCompletion(FutureTask.java:354)
                                                                                 at java.util.concurrent.FutureTask.setException(FutureTask.java:223)
                                                                                 at java.util.concurrent.FutureTask.run(FutureTask.java:242)
                                                                                 at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1113)
                                                                                 at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:588)
                                                                                 at java.lang.Thread.run(Thread.java:818)
                                                                              Caused by: java.lang.NullPointerException: uri
                                                                                 at com.android.internal.util.Preconditions.checkNotNull(Preconditions.java:60)
                                                                                 at android.content.ContentResolver.query(ContentResolver.java:474)
                                                                                 at android.content.CursorLoader.loadInBackground(CursorLoader.java:64)
                                                                                 at android.content.CursorLoader.loadInBackground(CursorLoader.java:56)
                                                                                 at android.content.AsyncTaskLoader.onLoadInBackground(AsyncTaskLoader.java:312)
                                                                                 at android.content.AsyncTaskLoader$LoadTask.doInBackground(AsyncTaskLoader.java:69)
                                                                                 at android.content.AsyncTaskLoader$LoadTask.doInBackground(AsyncTaskLoader.java:66)
                                                                                 at android.os.AsyncTask$2.call(AsyncTask.java:295)
                                                                                 at java.util.concurrent.FutureTask.run(FutureTask.java:237)
                                                                                 at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1113) 
                                                                                 at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:588) 
                                                                                 at java.lang.Thread.run(Thread.java:818) 

Here is the code for the fragment that contains the ListView

public class fragment_medications extends Fragment  implements LoaderManager.LoaderCallbacks<Cursor> {

    private database_helper_medications dbHelper;
    private SimpleCursorAdapter listAdapter;
    private SQLLiteCursorLoader loader=null;

    @Nullable
    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        //return super.onCreateView(inflater, container, savedInstanceState);

        View view = inflater.inflate(R.layout.fragment_medications_layout, container,false);

        dbHelper = new database_helper_medications(getActivity());

        listAdapter = new SimpleCursorAdapter(getActivity(),R.layout.list_item_medications,null, new String[] {database_helper_medications.FIELD_MEDICATION}, new int[] {R.id.tvMedication},0);

        ListView lvMedications = (ListView) view.findViewById(R.id.lvMedications);
        lvMedications.setAdapter(listAdapter);

        return view;
    }

    @Override public void onActivityCreated(Bundle savedInstanceState) {
        super.onActivityCreated(savedInstanceState);
        getLoaderManager().initLoader(0, null, this);
    }

    //TODO: 03-08 00:16:48.563 3508-3516/com.overunitystudios.medbuddy W/SQLiteConnectionPool: A SQLiteConnection object for database '/data/user/0/com.overunitystudios.medbuddy/databases/medbuddy' was leaked!  Please fix your application to end transactions in progress properly and to close the database when it is no longer needed.

    @Override
    public void onDestroy() {
        super.onDestroy();
        if (dbHelper!=null) dbHelper.close();
    }

    @Override
    public Loader<Cursor> onCreateLoader(int id, Bundle args) {
        loader=new SQLLiteCursorLoader(getActivity(), dbHelper, dbHelper.DATABASE_SELECT_ALL, null);
        return(loader);
    }


    @Override
    public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
        this.loader=(SQLLiteCursorLoader)loader;
        listAdapter.changeCursor(data);
    }

    @Override
    public void onLoaderReset(Loader<Cursor> loader) {
        listAdapter.changeCursor(null);
    }

}

The database helper

public class database_helper_medications extends SQLiteOpenHelper {

    private static final String DATABASE_NAME = "medbuddy";
    private static final String TABLE_NAME = "medications";
    private static final int DATABASE_VERSION = 1;

    public final static String FIELD_ID="_id"; //Changed table to use a column named "_id" instead of "id" as CursorAdapter must have an _id column in the table it is using.
    public final static String FIELD_MEDICATION="name";

    private SQLiteDatabase database;

    // Database creation sql statement
    private static final String DATABASE_CREATE = "create table if not exists " + TABLE_NAME + " ( " + FIELD_ID + " integer primary key," + FIELD_MEDICATION + " text not null);";
    public static final String DATABASE_SELECT_ALL = "select " + FIELD_ID + "," + FIELD_MEDICATION + " from " + TABLE_NAME + " order by " + FIELD_MEDICATION;

    public database_helper_medications(Context context) {
        super(context, DATABASE_NAME, null, DATABASE_VERSION);
        database = this.getWritableDatabase();
    }

    // Method is called during creation of the database
    @Override
    public void onCreate(SQLiteDatabase database) {
        database.execSQL(DATABASE_CREATE);
    }

    // Method is called during an upgrade of the database,
    @Override
    public void onUpgrade(SQLiteDatabase database,int oldVersion,int newVersion){
        database.execSQL("DROP TABLE IF EXISTS " + TABLE_NAME + "");
        onCreate(database);
    }

    public long createSingleRecord(Integer id, String medication){
        ContentValues values = new ContentValues();
        values.put(FIELD_ID, id);
        values.put(FIELD_MEDICATION, medication);
        return database.insert(TABLE_NAME, null, values);
    }

    public long createSingleRecord(String medication){
        ContentValues values = new ContentValues();
        //values.put(FIELD_ID, id); //We dont need to do this as it's marked autoincrement.
        values.put(FIELD_MEDICATION, medication);
        return database.insert(TABLE_NAME, null, values);
    }


    public Cursor selectAllRecords() {
        String[] cols = new String[] {FIELD_ID, FIELD_MEDICATION};
        Cursor mCursor = database.query(true, TABLE_NAME, cols,null, null, null, null, null, null);
        if (mCursor != null)
            mCursor.moveToFirst();

        return mCursor; // iterate to get each value.
    }

    public void DumpToLog()
    {
        Log.i("DumpToLog", DatabaseUtils.dumpCursorToString(selectAllRecords()));
    }
}

Here is SQLLiteCursorLoader (I renamed the popular SQLiteCursorLoader)

public class SQLLiteCursorLoader extends CursorLoader
{

    SQLiteOpenHelper db=null;
    String rawQuery=null;
    String[] args=null;
    /**
     * Creates a fully-specified SQLiteCursorLoader. See
     * {@link SQLiteDatabase#rawQuery(SQLiteDatabase, String, String[])
     * SQLiteDatabase.rawQuery()} for documentation on the
     * meaning of the parameters. These will be passed as-is
     * to that call.
     */
    public SQLLiteCursorLoader(Context context, SQLiteOpenHelper db, String rawQuery, String[] args)
    {
        super(context);
        this.db=db;
        this.rawQuery=rawQuery;
        this.args=args;
    }

    /**
     * Runs on a worker thread and performs the actual
     * database query to retrieve the Cursor.
     */
    //@Override
    protected Cursor buildCursor() {
        return(db.getReadableDatabase().rawQuery(rawQuery, args));
    }

    /**
     * Writes a semi-user-readable roster of contents to
     * supplied output.
     */
    @Override
    public void dump(String prefix, FileDescriptor fd,PrintWriter writer, String[] args)
    {
        super.dump(prefix, fd, writer, args);
        writer.print(prefix);
        writer.print("rawQuery=");
        writer.println(rawQuery);
        writer.print(prefix);
        writer.print("args=");
        writer.println(Arrays.toString(args));
    }

    public void insert(String table, String nullColumnHack, ContentValues values)
    {
        buildInsertTask(this).execute(db, table, nullColumnHack, values);
    }

    public void update(String table, ContentValues values,String whereClause, String[] whereArgs)
    {
        buildUpdateTask(this).execute(db, table, values, whereClause,whereArgs);
    }

    public void replace(String table, String nullColumnHack,ContentValues values)
    {
        buildReplaceTask(this).execute(db, table, nullColumnHack, values);
    }

    public void delete(String table, String whereClause,String[] whereArgs)
    {
        buildDeleteTask(this).execute(db, table, whereClause, whereArgs);
    }

    public void execSQL(String sql, Object[] bindArgs)
    {
        buildExecSQLTask(this).execute(db, sql, bindArgs);
    }

    protected ContentChangingTask buildInsertTask(SQLLiteCursorLoader loader)
    {
        return(new InsertTask(loader));
    }

    protected ContentChangingTask buildUpdateTask(SQLLiteCursorLoader loader)
    {
        return(new UpdateTask(loader));
    }

    protected ContentChangingTask buildReplaceTask(SQLLiteCursorLoader loader)
    {
        return(new ReplaceTask(loader));
    }

    protected ContentChangingTask buildDeleteTask(SQLLiteCursorLoader loader)
    {
        return(new DeleteTask(loader));
    }

    protected ContentChangingTask buildExecSQLTask(SQLLiteCursorLoader loader)
    {
        return(new ExecSQLTask(loader));
    }

    protected static class InsertTask extends ContentChangingTask
    {
        InsertTask(SQLLiteCursorLoader loader)
        {
            super(loader);
        }

        @Override
        protected Void doInBackground(Object... params) {
            SQLiteOpenHelper db=(SQLiteOpenHelper)params[0];
            String table=(String)params[1];
            String nullColumnHack=(String)params[2];
            ContentValues values=(ContentValues)params[3];

            db.getWritableDatabase().insert(table, nullColumnHack, values);

            return(null);
        }
    }

    protected static class UpdateTask extends ContentChangingTask
    {
        UpdateTask(SQLLiteCursorLoader loader)
        {
            super(loader);
        }

        @Override
        protected Void doInBackground(Object... params) {
            SQLiteOpenHelper db=(SQLiteOpenHelper)params[0];
            String table=(String)params[1];
            ContentValues values=(ContentValues)params[2];
            String where=(String)params[3];
            String[] whereParams=(String[])params[4];

            db.getWritableDatabase().update(table, values, where, whereParams);

            return(null);
        }
    }

    protected static class ReplaceTask extends ContentChangingTask
    {
        ReplaceTask(SQLLiteCursorLoader loader)
        {
            super(loader);
        }

        @Override
        protected Void doInBackground(Object... params) {
            SQLiteOpenHelper db=(SQLiteOpenHelper)params[0];
            String table=(String)params[1];
            String nullColumnHack=(String)params[2];
            ContentValues values=(ContentValues)params[3];

            db.getWritableDatabase().replace(table, nullColumnHack, values);

            return(null);
        }
    }

    protected static class DeleteTask extends ContentChangingTask
    {
        DeleteTask(SQLLiteCursorLoader loader)
        {
            super(loader);
        }

        @Override
        protected Void doInBackground(Object... params) {
            SQLiteOpenHelper db=(SQLiteOpenHelper)params[0];
            String table=(String)params[1];
            String where=(String)params[2];
            String[] whereParams=(String[])params[3];

            db.getWritableDatabase().delete(table, where, whereParams);

            return(null);
        }
    }

    protected static class ExecSQLTask extends ContentChangingTask
    {
        ExecSQLTask(SQLLiteCursorLoader loader)
        {
            super(loader);
        }

        @Override
        protected Void doInBackground(Object... params) {
            SQLiteOpenHelper db=(SQLiteOpenHelper)params[0];
            String sql=(String)params[1];
            Object[] bindParams=(Object[])params[2];

            db.getWritableDatabase().execSQL(sql, bindParams);

            return(null);
        }
    }
}

I'll not include the layout XML, as as I said earlier if I populate directly from an adapter and don't use a loader it works.


Solution

  • From the CursorLoader API,

    public CursorLoader (Context context)

    Added in API level 11 Creates an empty unspecified CursorLoader. You must follow this with calls to setUri(Uri), setSelection(String), etc to specify the query to perform.

    So in your custom SQLLiteCursorLoader class you must explicitly call setUri(Uri) in your constructor after super(context);

    UPDATE

    Looking at the github project you mention in your comment below, I spot one difference.

    Yours

    public class SQLLiteCursorLoader extends CursorLoader 
    

    while github's

    public class SQLiteCursorLoader extends AbstractCursorLoader
    

    And guess what, CursorLoader has a method (link)

    /* Runs on a worker thread */
    @Override
    public Cursor loadInBackground() {
        Cursor cursor = getContext().getContentResolver().query(mUri, mProjection, mSelection,
                mSelectionArgs, mSortOrder);
        if (cursor != null) {
            // Ensure the cursor window is filled
            cursor.getCount();
            registerContentObserver(cursor, mObserver);
        }
        return cursor;
    }
    

    which uses mUri that's why it cannot be null, whereas the equivalent method in AbstractCursorLoader (link)is:

    @Override
    public Cursor loadInBackground() {
        Cursor cursor=buildCursor();
    
        if (cursor!=null) {
          // Ensure the cursor window is filled
          cursor.getCount();
        }
    
        return(cursor);
    }
    

    So I suppose if you correct the inheritance in yours SQLLiteCursorLoader class (make it extend AbstractCursorLoader) you will eventually solve your problem.