InMemoryCursor.java

/*
 * Copyright (C) 2017 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package androidx.contentpager.content;

import static androidx.core.util.Preconditions.checkArgument;

import android.database.AbstractCursor;
import android.database.ContentObserver;
import android.database.Cursor;
import android.database.CursorIndexOutOfBoundsException;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;

import androidx.annotation.RestrictTo;

/**
 * A {@link Cursor} implementation that stores information in-memory, in a type-safe fashion.
 * Values are stored, when possible, as primitives to avoid the need for the autoboxing (as is
 * necessary when working with MatrixCursor).
 *
 * <p>Unlike {@link android.database.MatrixCursor}, this cursor is not mutable at runtime.
 * It exists solely as a destination for data copied by {@link ContentPager} from a source
 * Cursor when a page is being synthesized. It is not anticipated at this time that this
 * will be useful outside of this package. As such it is immutable once constructed.
 * @hide
 */
@RestrictTo(RestrictTo.Scope.LIBRARY)
final class InMemoryCursor extends AbstractCursor {

    private static final int NUM_TYPES = 5;

    private final String[] mColumnNames;
    private final int mRowCount;

    // This is an index of column, by type. Maps back to position
    // in native array.
    // E.g. if we have columns typed like [string, int, int, string, int, int]
    // the values in this index will be:
    // mTypedColumnIndex[string][0] == 0
    // mTypedColumnIndex[int][1] == 0
    // mTypedColumnIndex[int][2] == 1
    // mTypedColumnIndex[string][3] == 1
    // mTypedColumnIndex[int][4] == 2
    // mTypedColumnIndex[int][4] == 3
    // This allows us to calculate the number of cells by type in a row
    // which, in turn, allows us to calculate the size of the primitive storage arrays.
    // We also use this information to lookup a particular typed value given
    // the row offset and column offset.
    private final int[][] mTypedColumnIndex;

    // simple index to column to type.
    private final int[] mColumnType;

    // count of number of columns by type.
    private final int[] mColumnTypeCount;

    private final Bundle mExtras;

    private final ObserverRelay mObserverRelay;

    // Row data decomposed by type.
    private long[] mLongs;
    private double[] mDoubles;
    private byte[][] mBlobs;
    private String[] mStrings;

    /**
     * @param cursor source of data to copy. Ownership is reserved to the called, meaning
     *               we won't ever close it.
     */
    InMemoryCursor(Cursor cursor, int offset, int length, int disposition) {
        checkArgument(offset < cursor.getCount());

        // NOTE: The cursor could simply be saved to a field, but we choose to wrap
        // in a dedicated relay class to avoid hanging directly onto a reference
        // to the cursor...so future authors are not enticed to think there's
        // a live link between the delegate cursor and this cursor.
        mObserverRelay = new ObserverRelay(cursor);

        mColumnNames = cursor.getColumnNames();
        mRowCount = Math.min(length, cursor.getCount() - offset);
        int numColumns = cursor.getColumnCount();

        mExtras = ContentPager.buildExtras(cursor.getExtras(), cursor.getCount(), disposition);

        mColumnType = new int[numColumns];
        mTypedColumnIndex = new int[NUM_TYPES][numColumns];
        mColumnTypeCount = new int[NUM_TYPES];

        if (!cursor.moveToFirst()) {
            throw new RuntimeException("Can't position cursor to first row.");
        }

        for (int col = 0; col < numColumns; col++) {
            int type = cursor.getType(col);
            mColumnType[col] = type;
            mTypedColumnIndex[type][col] = mColumnTypeCount[type]++;
        }

        mLongs = new long[mRowCount * mColumnTypeCount[FIELD_TYPE_INTEGER]];
        mDoubles = new double[mRowCount * mColumnTypeCount[FIELD_TYPE_FLOAT]];
        mBlobs = new byte[mRowCount * mColumnTypeCount[FIELD_TYPE_BLOB]][];
        mStrings = new String[mRowCount * mColumnTypeCount[FIELD_TYPE_STRING]];

        for (int row = 0; row < mRowCount; row++) {
            if (!cursor.moveToPosition(offset + row)) {
                throw new RuntimeException("Unable to position cursor.");
            }

            // Now copy data from the row into primitive arrays.
            for (int col = 0; col < mColumnType.length; col++) {
                int type = mColumnType[col];
                int position = getCellPosition(row, col, type);

                switch(type) {
                    case FIELD_TYPE_NULL:
                        throw new UnsupportedOperationException("Not implemented.");
                    case FIELD_TYPE_INTEGER:
                        mLongs[position] = cursor.getLong(col);
                        break;
                    case FIELD_TYPE_FLOAT:
                        mDoubles[position] = cursor.getDouble(col);
                        break;
                    case FIELD_TYPE_BLOB:
                        mBlobs[position] = cursor.getBlob(col);
                        break;
                    case FIELD_TYPE_STRING:
                        mStrings[position] = cursor.getString(col);
                        break;
                }
            }
        }
    }

    @Override
    public Bundle getExtras() {
        return mExtras;
    }

    // Returns the "cell" position for a specific row+column+type.
    private int getCellPosition(int row,  int col, int type) {
        return (row * mColumnTypeCount[type]) + mTypedColumnIndex[type][col];
    }

    @Override
    public int getCount() {
        return mRowCount;
    }

    @Override
    public String[] getColumnNames() {
        return mColumnNames;
    }

    @Override
    public short getShort(int column) {
        checkValidColumn(column);
        checkValidPosition();
        return (short) mLongs[getCellPosition(getPosition(), column, FIELD_TYPE_INTEGER)];
    }

    @Override
    public int getInt(int column) {
        checkValidColumn(column);
        checkValidPosition();
        return (int) mLongs[getCellPosition(getPosition(), column, FIELD_TYPE_INTEGER)];
    }

    @Override
    public long getLong(int column) {
        checkValidColumn(column);
        checkValidPosition();
        return mLongs[getCellPosition(getPosition(), column, FIELD_TYPE_INTEGER)];
    }

    @Override
    public float getFloat(int column) {
        checkValidColumn(column);
        checkValidPosition();
        return (float) mDoubles[getCellPosition(getPosition(), column, FIELD_TYPE_FLOAT)];
    }

    @Override
    public double getDouble(int column) {
        checkValidColumn(column);
        checkValidPosition();
        return mDoubles[getCellPosition(getPosition(), column, FIELD_TYPE_FLOAT)];
    }

    @Override
    public byte[] getBlob(int column) {
        checkValidColumn(column);
        checkValidPosition();
        return mBlobs[getCellPosition(getPosition(), column, FIELD_TYPE_BLOB)];
    }

    @Override
    public String getString(int column) {
        checkValidColumn(column);
        checkValidPosition();
        return mStrings[getCellPosition(getPosition(), column, FIELD_TYPE_STRING)];
    }

    @Override
    public int getType(int column) {
        checkValidColumn(column);
        return mColumnType[column];
    }

    @Override
    public boolean isNull(int column) {
        checkValidColumn(column);
        switch (mColumnType[column]) {
            case FIELD_TYPE_STRING:
                return getString(column) != null;
            case FIELD_TYPE_BLOB:
                return getBlob(column) != null;
            default:
                return false;
        }
    }

    private void checkValidPosition() {
        if (getPosition() < 0) {
            throw new CursorIndexOutOfBoundsException("Before first row.");
        }
        if (getPosition() >= mRowCount) {
            throw new CursorIndexOutOfBoundsException("After last row.");
        }
    }

    private void checkValidColumn(int column) {
        if (column < 0 || column >= mColumnNames.length) {
            throw new CursorIndexOutOfBoundsException(
                    "Requested column: " + column + ", # of columns: " + mColumnNames.length);
        }
    }

    @Override
    public void registerContentObserver(ContentObserver observer) {
        mObserverRelay.registerContentObserver(observer);
    }

    @Override
    public void unregisterContentObserver(ContentObserver observer) {
        mObserverRelay.unregisterContentObserver(observer);
    }

    private static class ObserverRelay extends ContentObserver {
        private final Cursor mCursor;

        ObserverRelay(Cursor cursor) {
            super(new Handler(Looper.getMainLooper()));
            mCursor = cursor;
        }

        void registerContentObserver(ContentObserver observer) {
            mCursor.registerContentObserver(observer);
        }

        void unregisterContentObserver(ContentObserver observer) {
            mCursor.unregisterContentObserver(observer);
        }
    }
}