| /* |
| * 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); |
| } |
| } |
| } |