Search code examples
androidparcelableandroid-cursor

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!


Solution

  • 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 [email protected]
     * 
     */
    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.