Q:

Passing a Cursor between processes (Parcelable Cursor)

I need to pass a Cursor (SQLiteCursor) from a service to an application on API 10, and having hard time finding a decent (and fast) solution.
I've seen the CursorWindow class. This is Parcelable but I can't instantiate this class on API 10 to use SQLiteCursor.fillWindow() because it has no valid constructors. CursorWindow(boolean) is deprecated.
And even if I got a CursorWindow instance with data from a SQLiteCursor, how do I copy this window into a new Cursor? What Cursor implementation should I use for this? I see no usable Cursor that extends AbstractWindowedCursor.
Thanks for your time!

A:

I implemented a ParcelableCursor class that implements CrossProcessCursor and Parcelable interfaces. I'll post it if anyone is interested. Some operations are not supported/implemented yet, as well as using a custom BijectiveMap (which is quite easy to implement).
/**
 * Prefer ParcelableCursorForIntent instead.<br/>
 * Cursor for IPC. Takes a CursorWindow as data buffer and the number of columns
 * that CursorWindow has.<br/>
 * <br/>
 * <b>NOTE: this Cursor cannot be parceled when sending by intents due to <a
 * href="http://code.google.com/p/android/issues/detail?id=4470">an Android
 * bug</a>. Please use ParcelableCursorForIntent instead.</b>
 * 
 * @author m0skit0@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;

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

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

    /**
     * Creates an empty ParcelableCursor. Please consider to use
     * {@link #setFromCursor(AbstractWindowedCursor)} or
     * {@link #setFromWindow(CursorWindow)} to initialize it.
     */
    public ParcelableCursor() {
        // Empty ParcelableCursor, don't forget to use #setFromCursor
    }

    /** Constructor for Parcelable */
    public ParcelableCursor(final Parcel in) {
        readFromParcel(in); // NOPMD by yasin on 12/7/12 11:55 AM - Android's
        // Parceleble
    }

    /**
     * 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(final String name) {
        this.numColumns++;
        this.curRow = -1;
        this.colNames.put(name, this.numColumns - 1);
    }

    @Override
    public void close() {
        this.window.close();
        this.closed = true;
    }

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

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

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

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

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

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

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

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

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

    @Override
    public String[] getColumnNames() {
        if (DebugConfig.DEBUG) {
            Log.d("PARCELCURSOR.getColumnNames()---", "===GETTING COLNAMES===");
        }

        final Set<Entry<String, Integer>> set = this.colNames.entrySet();
        final String[] colArray = new String[set.size()];
        for (final String colName : this.colNames.keySet()) {
            if (DebugConfig.DEBUG) {
                Log.d("-------------PARCELCURSOR.getColumnNames()", colName);
            }
            final int pos = this.colNames.get(colName);
            colArray[pos] = colName;
        }

        return colArray;
    }

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

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

    @Override
    public Bundle getExtras() {
        // Does not support Extras
        return null;
    }

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

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

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

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

    @Override
    public short getShort(final int columnIndex) { // NOPMD by yasin on 12/7/12
                                                    // 11:57 AM - Override
        return this.window.getShort(this.curRow, columnIndex);
    }

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

    @SuppressLint("NewApi")
    @Override
    public int getType(final int columnIndex) {
        final int currentapiVersion = android.os.Build.VERSION.SDK_INT;

        int result = 0;

        if (currentapiVersion >= android.os.Build.VERSION_CODES.HONEYCOMB) {
            result = this.window.getType(this.curRow, columnIndex);
        } else {
            if (this.window.isNull(this.curRow, columnIndex)) {
                result = 0; // FIELD_TYPE_NULL;
            } else if (this.window.isFloat(this.curRow, columnIndex)) {
                result = 2; // FIELD_TYPE_FLOAT;
            } else if (this.window.isLong(this.curRow, columnIndex)) {
                result = 1; // FIELD_TYPE_INTEGER;
            } else if (this.window.isString(this.curRow, columnIndex)) {
                result = 3; // FIELD_TYPE_STRING;
            } else if (this.window.isBlob(this.curRow, columnIndex)) {
                result = 4; // FIELD_TYPE_BLOB;
            }
        }

        return result;
    }

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

    @Override
    public CursorWindow getWindow() {
        final CursorWindow ret = CursorHelper.getCursorWindowInstance();
        fillWindow(0, ret);
        return ret;
    }

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

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

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

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

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

    @Override
    public boolean isNull(final int columnIndex) {
        return this.getType(columnIndex) == FIELD_TYPE_NULL;
    }

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

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

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

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

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

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

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

    /** Restoring this object from a Parcel */
    public void readFromParcel(final Parcel in) {

        this.numColumns = in.readInt();
        this.colNames = in.readParcelable(ClassLoaderHelper.getClassLoader());
        this.curRow = in.readInt();
        this.closed = (in.readByte() == 1);
        // Closes the cursor before create a new cursor.
        if (window != null) {
            window.close();
        }
        this.window = CursorWindow.newFromParcel(in);
    }

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

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

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

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

    /** Sets this cursor from another windowed Cursor */
    public void setFromCursor(final AbstractWindowedCursor cursor) throws CursorIndexOutOfBoundsException, IllegalStateException {

        // Reset number of columns
        this.numColumns = 0;

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

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

    /** Sets this cursor from another windowed Cursor */
    public void setFromCursor(final MatrixCursor cursor) throws CursorIndexOutOfBoundsException ,IllegalStateException{

        // Reset number of columns
        this.numColumns = 0;

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

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

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

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

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

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

    @Override
    public void writeToParcel(final Parcel out, final int flags) {

        out.writeInt(this.numColumns);
        out.writeParcelable((Parcelable) this.colNames, 0);
        out.writeInt(this.curRow);
        out.writeByte(this.closed ? (byte) 1 : 0);
        this.window.writeToParcel(out, flags);
    }

}

Still looking for a more standard way to do this. Any information would be appreciated greatly!
EDIT: this passed very few tests, so test it before using it.
EDIT2: in fact, it's full of bugs... I'll update with a less buggy version soon.
EDIT3: updated with working cursor we are using since one year.

