0

I have this Cursor implementation (API 10):

package com.blablabla.android.helpers.db.cursor;

import android.content.ContentResolver;
import android.database.AbstractWindowedCursor;
import android.database.CharArrayBuffer;
import android.database.ContentObserver;
import android.database.CrossProcessCursor;
import android.database.CursorWindow;
import android.database.DataSetObserver;
import android.net.Uri;
import android.os.Bundle;
import android.os.Parcel;
import android.os.Parcelable;
import android.util.Log;

import com.blablabla.android.helpers.classloader.ClassLoaderHelper;
import com.blablabla.android.helpers.util.BijectiveHashMap;
import com.blablabla.android.helpers.util.BijectiveMap;

/**
 * Cursor for IPC. Takes a CursorWindow as data buffer and the number of columns
 * that CursorWindow has.
 * 
 * @author me@blablabla.eu
 * 
 */
public class ParcelableCursor implements Parcelable, CrossProcessCursor {

    /** Cursor data window */
    protected CursorWindow window = CursorHelper.getCursorWindowInstance();

    /** How many columns we have */
    protected int numColumns = 0;

    /** Column names */
    protected BijectiveMap<String, Integer> colNames = new BijectiveHashMap<String, Integer>();

    /** Current row */
    protected int curRow = -1;

    /** Is this cursor closed? */
    protected boolean closed = false;

    public ParcelableCursor() {
    }

    // /////////////////
    // PARCELABLE IMPLEMENTATION
    // /////////////////

    /** CREATOR for Parcelable */
    public static final Parcelable.Creator<ParcelableCursor> CREATOR = new Parcelable.Creator<ParcelableCursor>() {
        public ParcelableCursor createFromParcel(Parcel in) {
            return new ParcelableCursor(in);
        }

        public ParcelableCursor[] newArray(int size) {
            return new ParcelableCursor[size];
        }
    };

    /** Constructor for Parcelable */
    public ParcelableCursor(Parcel in) {
        readFromParcel(in);
    }

    @Override
    public int describeContents() {
        // Nothing to do here
        return 0;
    }

    @Override
    public void writeToParcel(Parcel out, int flags) {
        out.writeParcelable(window, 0);
        out.writeInt(numColumns);
        out.writeParcelable((Parcelable) colNames, 0);
        out.writeInt(curRow);
        out.writeByte(closed ? (byte) 1 : 0);
    }

    /** Restoring this object from a Parcel */
    public void readFromParcel(Parcel in) {
        window = in.readParcelable(CursorWindow.class.getClassLoader());
        numColumns = in.readInt();
        colNames = in.readParcelable(ClassLoaderHelper.getClassLoader());
        curRow = in.readInt();
        closed = (in.readByte() == 1);
    }

    // ////////////////
    // END PARCELABLE IMPLEMENTATION
    // ////////////////

    // /////////////////
    // CROSS PROCESS CURSOR IMPLEMENTATION
    // /////////////////
    @Override
    public void close() {
        window.close();
        closed = true;
    }

    @Override
    public void copyStringToBuffer(int columnIndex, CharArrayBuffer buffer) {
        // TODO: what does this do?
    }

    @Override
    public void deactivate() {
        // Deprecated, does nothing
    }

    @Override
    public byte[] getBlob(int columnIndex) {
        return window.getBlob(curRow, columnIndex);
    }

    @Override
    public int getColumnCount() {
        return numColumns;
    }

    @Override
    public int getColumnIndex(String columnName) {
        int ret = -1;
        Integer col = colNames.get(columnName);
        if (col != null) {
            ret = col;
        }
        return ret;
    }

    @Override
    public int getColumnIndexOrThrow(String columnName)
            throws IllegalArgumentException {
        Integer col = colNames.get(columnName);
        if (col == null) {
            throw new IllegalArgumentException();
        }
        return col;
    }

    @Override
    public String getColumnName(int columnIndex) {
        return colNames.getKey(columnIndex);
    }

    @Override
    public String[] getColumnNames() {
        Log.d("PARCELCURSOR.getColumnNames()", "===GETTING COLNAMES===");
        return colNames.keySet().toArray(new String[colNames.keySet().size()]);
    }

    @Override
    public int getCount() {
        return window.getNumRows();
    }

    @Override
    public double getDouble(int columnIndex) {
        return window.getDouble(curRow, columnIndex);
    }

    @Override
    public Bundle getExtras() {
        // TODO
        return null;
    }

    @Override
    public float getFloat(int columnIndex) {
        return window.getFloat(curRow, columnIndex);
    }

    @Override
    public int getInt(int columnIndex) {
        return window.getInt(curRow, columnIndex);
    }

    @Override
    public long getLong(int columnIndex) {
        return window.getLong(curRow, columnIndex);
    }

    @Override
    public int getPosition() {
        return curRow;
    }

    @Override
    public short getShort(int columnIndex) {
        return window.getShort(curRow, columnIndex);
    }

    @Override
    public String getString(int columnIndex) {
        return window.getString(curRow, columnIndex);
    }

    @Override
    public boolean getWantsAllOnMoveCalls() {
        return false;
    }

    @Override
    public boolean isAfterLast() {
        return (curRow >= window.getNumRows());
    }

    @Override
    public boolean isBeforeFirst() {
        return (curRow < 0);
    }

    @Override
    public boolean isClosed() {
        return closed;
    }

    @Override
    public boolean isFirst() {
        return (curRow == 0);
    }

    @Override
    public boolean isLast() {
        return (curRow == window.getNumRows() - 1);
    }

    @Override
    public boolean isNull(int columnIndex) {
        return window.isNull(curRow, columnIndex);
    }

    @Override
    public boolean move(int offset) {
        int oldPos = curRow;
        curRow += offset;
        if (curRow < -1) {
            curRow = -1;
            return false;
        } else if (curRow > window.getNumRows() - 1) {
            curRow = window.getNumRows() - 1;
            return false;
        }
        return onMove(oldPos, curRow);
    }

    @Override
    public boolean moveToFirst() {
        if (window.getNumRows() == 0) {
            return false;
        }
        int oldPos = curRow;
        curRow = 0;
        return onMove(oldPos, curRow);
    }

    @Override
    public boolean moveToLast() {
        if (window.getNumRows() == 0) {
            return false;
        }
        int oldPos = curRow;
        curRow = window.getNumRows() - 1;
        return onMove(oldPos, curRow);
    }

    @Override
    public boolean moveToNext() {
        int oldPos = curRow++;
        if (isAfterLast()) {
            curRow = window.getNumRows();
            return false;
        }
        return onMove(oldPos, curRow);
    }

    @Override
    public boolean moveToPosition(int position) {
        if (position < -1 && position >= window.getNumRows()) {
            return false;
        }
        int oldPos = curRow;
        curRow = position;
        return onMove(oldPos, curRow);
    }

    @Override
    public boolean moveToPrevious() {
        int oldPos = curRow--;
        if (isBeforeFirst()) {
            curRow = -1;
            return false;
        }
        return onMove(oldPos, curRow);
    }

    /** Not supported */
    @Override
    public void registerContentObserver(ContentObserver observer) {
        // Does nothing
    }

    /** Not supported */
    @Override
    public void registerDataSetObserver(DataSetObserver observer) {
        // Does nothing
    }

    /** Deprecated, not supported */
    @Override
    public boolean requery() {
        return false;
    }

    /** Not supported */
    @Override
    public Bundle respond(Bundle extras) {
        // Does nothing
        return null;
    }

    /** Not supported */
    @Override
    public void setNotificationUri(ContentResolver cr, Uri uri) {
        // Does nothing
    }

    /** Not supported */
    @Override
    public void unregisterContentObserver(ContentObserver observer) {
        // Does nothing
    }

    /** Not supported */
    @Override
    public void unregisterDataSetObserver(DataSetObserver observer) {
        // Does nothing
    }

    @Override
    public void fillWindow(int position, CursorWindow window) {
        CursorHelper.copyCursorWindow(position, this.window, window);
    }

    @Override
    public CursorWindow getWindow() {
        CursorWindow ret = new CursorWindow(false);
        fillWindow(0, ret);
        return ret;
    }

    @Override
    public boolean onMove(int oldPosition, int newPosition) {
        // Don't forget to set curRow = -1 if this method returns false
        return true;
    }

    // /////////////////
    // END CROSS PROCESS CURSOR IMPLEMENTATION
    // /////////////////

    /** Sets this cursor using a CursorWindow data */
    public void setFromWindow(CursorWindow window) {
        CursorHelper.copyCursorWindow(0, window, this.window);
        numColumns = CursorHelper.getCursorWindowNumCols(window);
        moveToPosition(-1);
    }

    /** Sets this cursor from another windowed Cursor */
    public void setFromCursor(AbstractWindowedCursor cursor) {

        // Set column names
        String[] colNames = cursor.getColumnNames();
        for (String col : colNames) {
            addColumn(col);
        }

        // Fill window
        window.setNumColumns(numColumns);
        cursor.fillWindow(0, window);
        moveToPosition(-1);
    }

    /**
     * Adds a new column at the end and assigns it this name. This will make
     * this cursor to lose all its data, so you have to add all the columns
     * before adding any row.
     */
    private void addColumn(String name) {
        numColumns++;
        curRow = -1;
        colNames.put(name, numColumns - 1);
    }
}

I send an instance of this Cursor through both Messenger (inside a Message instance) and through ContentProvider.query() (and friends). The problem is that isNull() method works correctly when the Cursor is sent through Messenger but always returns false when sent through the ContentProvider.

This is my ContentProvider.query() implementation:

@Override
public Cursor query(final Uri uri, final String[] projection,
    final String selection, final String[] selectionArgs,
    final String sortOrder) {

    // Build custom query object
    final Query query = getBasicQuery(uri);
    // .... More code preparing the query object

    // Execute the query
    dbManager.executeQuery(getSourceId(uri), query);

    // This is a ParcelableCursor instance
    final Cursor resultCursor = query.getResultCursor();
    return resultCursor;
}

Here, when I am running at the ContentProvider process, the isNull() method works properly, but when the Cursor is received at the client process, it always returns false.

And this is the exception thrown when I do a Cursor.getString() because Cursor.isNull() returns (wrongly) false:

08-13 13:17:16.480: D/SELECT on ui(1572): java.lang.IllegalStateException: UNKNOWN type 0
08-13 13:17:16.480: D/SELECT on ui(1572):         at android.database.CursorWindow.getString_native(Native Method)
08-13 13:17:16.480: D/SELECT on ui(1572):         at android.database.CursorWindow.getString(CursorWindow.java:329)
08-13 13:17:16.480: D/SELECT on ui(1572):         at android.database.AbstractWindowedCursor.getString(AbstractWindowedCursor.java:49)
08-13 13:17:16.480: D/SELECT on ui(1572):         at android.database.CursorWrapper.getString(CursorWrapper.java:135)
08-13 13:17:16.480: D/SELECT on ui(1572):         at com.blablabla.android.core.test.TestDBActivity.selectDB(TestDBActivity.java:332)
08-13 13:17:16.480: D/SELECT on ui(1572):         at com.blablabla.android.core.test.TestDBActivity$3.onClick(TestDBActivity.java:169)
08-13 13:17:16.480: D/SELECT on ui(1572):         at android.view.View.performClick(View.java:2485)
08-13 13:17:16.480: D/SELECT on ui(1572):         at android.view.View$PerformClick.run(View.java:9080)
08-13 13:17:16.480: D/SELECT on ui(1572):         at android.os.Handler.handleCallback(Handler.java:587)
08-13 13:17:16.480: D/SELECT on ui(1572):         at android.os.Handler.dispatchMessage(Handler.java:92)
08-13 13:17:16.480: D/SELECT on ui(1572):         at android.os.Looper.loop(Looper.java:123)
08-13 13:17:16.480: D/SELECT on ui(1572):         at android.app.ActivityThread.main(ActivityThread.java:3683)
08-13 13:17:16.480: D/SELECT on ui(1572):         at java.lang.reflect.Method.invokeNative(Native Method)
08-13 13:17:16.480: D/SELECT on ui(1572):         at java.lang.reflect.Method.invoke(Method.java:507)
08-13 13:17:16.480: D/SELECT on ui(1572):         at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:839)
08-13 13:17:16.480: D/SELECT on ui(1572):         at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:597)
08-13 13:17:16.480: D/SELECT on ui(1572):         at dalvik.system.NativeStart.main(Native Method)

EDIT: after heavy debugging, I can see that when the value is null, isBlob() returns true.

Any ideas of what might be going wrong here?

m0skit0
  • 25,268
  • 11
  • 79
  • 127
  • The main difference between your implementation and the `SQLiteCursor` impl. that I can see is your `#getWindow()` implementation, an `AbstractWindowedCursor` subclass would for instance not be constantly creating new `Window`s - but retain a single instance. – Jens Aug 13 '12 at 11:54
  • You mean I should return the `CursorWindow` that this object owns? But that can be unpredicatble if other objects use this method to change `CursorWindow` contents in an unexpected manner. Also what has this to do with `isNull()` not working properly? I can't see the relation. – m0skit0 Aug 13 '12 at 12:24
  • I changed it to simply `return window;` and still the same problem. – m0skit0 Aug 13 '12 at 12:29

1 Answers1

0

The problem was actually in CursorHelper.copyCursorWindow(), which is called through fillWindow(), which Android system calls within CursorWrapper. Here's the buggy version I had of this method:

/**
 * Copies one CursorWindow into another. Previous data in destination
 * CursorWindow is lost
 * 
 * @param position
 *            Starting position to copy from the origin window
 * @param numColumns
 *            Number of columns in the origin window
 * @param origin
 *            CursorWindow to copy from
 * @param destination
 *            CursorWindow to copy to
 */
public static void copyCursorWindow(int position, CursorWindow origin,
        CursorWindow destination) {

    // Column number
    int numCols = getCursorWindowNumCols(origin);

    // Clear destination
    destination.clear();
    destination.setNumColumns(numCols);

    // Rows
    int i = position;
    while (i < origin.getNumRows() && destination.allocRow()) {

        // Columns
        for (int j = 0; j < numCols; j++) {

            if (origin.isBlob(i, j)) {
                byte[] cur = origin.getBlob(i, j);
                destination.putBlob(cur, i, j);

            } else if (origin.isFloat(i, j)) {
                Float cur = origin.getFloat(i, j);
                destination.putDouble(cur, i, j);

            } else if (origin.isLong(i, j)) {
                Long cur = origin.getLong(i, j);
                destination.putLong(cur, i, j);

            } else if (origin.isString(i, j)) {
                String cur = origin.getString(i, j);
                destination.putString(cur, i, j);

            } else if (origin.isNull(i, j)) {
                destination.putNull(i, j);
            }
        }
        i++;
    }
}

The problem here is that isBlob() will return true when the value is actually null. So isNull() should be the first check. Here's an implementation that works:

/**
 * Copies one CursorWindow into another. Previous data in destination
 * CursorWindow is lost
 * 
 * @param position
 *            Starting position to copy from the origin window
 * @param numColumns
 *            Number of columns in the origin window
 * @param origin
 *            CursorWindow to copy from
 * @param destination
 *            CursorWindow to copy to
 */
public static void copyCursorWindow(int position, CursorWindow origin,
        CursorWindow destination) {

    // Column number
    int numCols = getCursorWindowNumCols(origin);

    // Clear destination
    destination.clear();
    destination.setNumColumns(numCols);

    // Rows
    int i = position;
    while (i < origin.getNumRows() && destination.allocRow()) {

        // Columns
        for (int j = 0; j < numCols; j++) {

            if (origin.isNull(i, j)) {
                destination.putNull(i, j);

            } else if (origin.isFloat(i, j)) {
                Float cur = origin.getFloat(i, j);
                destination.putDouble(cur, i, j);

            } else if (origin.isLong(i, j)) {
                Long cur = origin.getLong(i, j);
                destination.putLong(cur, i, j);

            } else if (origin.isString(i, j)) {
                String cur = origin.getString(i, j);
                destination.putString(cur, i, j);

            } else if (origin.isBlob(i, j)) {
                byte[] cur = origin.getBlob(i, j);
                destination.putBlob(cur, i, j);
            }
        }
        i++;
    }
}

IMHO this is a (annoying) bug in how isBlob() is implemented (although probably fixed in API 11+, because there were some changes in this part -like Cursor.getType() method-).

(Implementation for getCursorWindowNumCols())

Community
  • 1
  • 1
m0skit0
  • 25,268
  • 11
  • 79
  • 127