org.robolectric.shadows.ShadowLegacyCursorWindow Maven / Gradle / Ivy
package org.robolectric.shadows;
import static android.os.Build.VERSION_CODES.KITKAT_WATCH;
import static android.os.Build.VERSION_CODES.LOLLIPOP;
import static java.nio.charset.StandardCharsets.UTF_8;
import static org.robolectric.RuntimeEnvironment.castNativePtr;
import android.database.Cursor;
import android.database.CursorWindow;
import com.almworks.sqlite4java.SQLiteConstants;
import com.almworks.sqlite4java.SQLiteException;
import com.almworks.sqlite4java.SQLiteStatement;
import com.google.common.base.Preconditions;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicLong;
import org.robolectric.annotation.Implementation;
import org.robolectric.annotation.Implements;
/** Legacy shadow for {@link CursowWindow}. */
@Implements(value = CursorWindow.class, isInAndroidSdk = false)
public class ShadowLegacyCursorWindow extends ShadowCursorWindow {
private static final WindowData WINDOW_DATA = new WindowData();
@Implementation
protected static Number nativeCreate(String name, int cursorWindowSize) {
return castNativePtr(WINDOW_DATA.create(name, cursorWindowSize));
}
@Implementation(maxSdk = KITKAT_WATCH)
protected static void nativeDispose(int windowPtr) {
nativeDispose((long) windowPtr);
}
@Implementation(minSdk = LOLLIPOP)
protected static void nativeDispose(long windowPtr) {
WINDOW_DATA.close(windowPtr);
}
@Implementation(maxSdk = KITKAT_WATCH)
protected static byte[] nativeGetBlob(int windowPtr, int row, int column) {
return nativeGetBlob((long) windowPtr, row, column);
}
@Implementation(minSdk = LOLLIPOP)
protected static byte[] nativeGetBlob(long windowPtr, int row, int column) {
Value value = WINDOW_DATA.get(windowPtr).value(row, column);
switch (value.type) {
case Cursor.FIELD_TYPE_NULL:
return null;
case Cursor.FIELD_TYPE_BLOB:
// This matches Android's behavior, which does not match the SQLite spec
byte[] blob = (byte[])value.value;
return blob == null ? new byte[]{} : blob;
case Cursor.FIELD_TYPE_STRING:
// Matches the Android behavior to contain a zero-byte at the end
byte[] stringBytes = ((String) value.value).getBytes(UTF_8);
return Arrays.copyOf(stringBytes, stringBytes.length + 1);
default:
throw new android.database.sqlite.SQLiteException(
"Getting blob when column is non-blob. Row " + row + ", col " + column);
}
}
@Implementation(maxSdk = KITKAT_WATCH)
protected static String nativeGetString(int windowPtr, int row, int column) {
return nativeGetString((long) windowPtr, row, column);
}
@Implementation(minSdk = LOLLIPOP)
protected static String nativeGetString(long windowPtr, int row, int column) {
Value val = WINDOW_DATA.get(windowPtr).value(row, column);
if (val.type == Cursor.FIELD_TYPE_BLOB) {
throw new android.database.sqlite.SQLiteException(
"Getting string when column is blob. Row " + row + ", col " + column);
}
Object value = val.value;
return value == null ? null : String.valueOf(value);
}
@Implementation(maxSdk = KITKAT_WATCH)
protected static long nativeGetLong(int windowPtr, int row, int column) {
return nativeGetLong((long) windowPtr, row, column);
}
@Implementation(minSdk = LOLLIPOP)
protected static long nativeGetLong(long windowPtr, int row, int column) {
return nativeGetNumber(windowPtr, row, column).longValue();
}
@Implementation(maxSdk = KITKAT_WATCH)
protected static double nativeGetDouble(int windowPtr, int row, int column) {
return nativeGetDouble((long) windowPtr, row, column);
}
@Implementation(minSdk = LOLLIPOP)
protected static double nativeGetDouble(long windowPtr, int row, int column) {
return nativeGetNumber(windowPtr, row, column).doubleValue();
}
@Implementation(maxSdk = KITKAT_WATCH)
protected static int nativeGetType(int windowPtr, int row, int column) {
return nativeGetType((long) windowPtr, row, column);
}
@Implementation(minSdk = LOLLIPOP)
protected static int nativeGetType(long windowPtr, int row, int column) {
return WINDOW_DATA.get(windowPtr).value(row, column).type;
}
@Implementation(maxSdk = KITKAT_WATCH)
protected static void nativeClear(int windowPtr) {
nativeClear((long) windowPtr);
}
@Implementation(minSdk = LOLLIPOP)
protected static void nativeClear(long windowPtr) {
WINDOW_DATA.clear(windowPtr);
}
@Implementation(maxSdk = KITKAT_WATCH)
protected static int nativeGetNumRows(int windowPtr) {
return nativeGetNumRows((long) windowPtr);
}
@Implementation(minSdk = LOLLIPOP)
protected static int nativeGetNumRows(long windowPtr) {
return WINDOW_DATA.get(windowPtr).numRows();
}
@Implementation(maxSdk = KITKAT_WATCH)
protected static boolean nativePutBlob(int windowPtr, byte[] value, int row, int column) {
return nativePutBlob((long) windowPtr, value, row, column);
}
@Implementation(minSdk = LOLLIPOP)
protected static boolean nativePutBlob(long windowPtr, byte[] value, int row, int column) {
// Real Android will crash in native code if putString is called with a null value.
Preconditions.checkNotNull(value);
return WINDOW_DATA.get(windowPtr).putValue(new Value(value, Cursor.FIELD_TYPE_BLOB), row, column);
}
@Implementation(maxSdk = KITKAT_WATCH)
protected static boolean nativePutString(int windowPtr, String value, int row, int column) {
return nativePutString((long) windowPtr, value, row, column);
}
@Implementation(minSdk = LOLLIPOP)
protected static boolean nativePutString(long windowPtr, String value, int row, int column) {
// Real Android will crash in native code if putString is called with a null value.
Preconditions.checkNotNull(value);
return WINDOW_DATA.get(windowPtr).putValue(new Value(value, Cursor.FIELD_TYPE_STRING), row, column);
}
@Implementation(maxSdk = KITKAT_WATCH)
protected static boolean nativePutLong(int windowPtr, long value, int row, int column) {
return nativePutLong((long) windowPtr, value, row, column);
}
@Implementation(minSdk = LOLLIPOP)
protected static boolean nativePutLong(long windowPtr, long value, int row, int column) {
return WINDOW_DATA.get(windowPtr).putValue(new Value(value, Cursor.FIELD_TYPE_INTEGER), row, column);
}
@Implementation(maxSdk = KITKAT_WATCH)
protected static boolean nativePutDouble(int windowPtr, double value, int row, int column) {
return nativePutDouble((long) windowPtr, value, row, column);
}
@Implementation(minSdk = LOLLIPOP)
protected static boolean nativePutDouble(long windowPtr, double value, int row, int column) {
return WINDOW_DATA.get(windowPtr).putValue(new Value(value, Cursor.FIELD_TYPE_FLOAT), row, column);
}
@Implementation(maxSdk = KITKAT_WATCH)
protected static boolean nativePutNull(int windowPtr, int row, int column) {
return nativePutNull((long) windowPtr, row, column);
}
@Implementation(minSdk = LOLLIPOP)
protected static boolean nativePutNull(long windowPtr, int row, int column) {
return WINDOW_DATA.get(windowPtr).putValue(new Value(null, Cursor.FIELD_TYPE_NULL), row, column);
}
@Implementation(maxSdk = KITKAT_WATCH)
protected static boolean nativeAllocRow(int windowPtr) {
return nativeAllocRow((long) windowPtr);
}
@Implementation(minSdk = LOLLIPOP)
protected static boolean nativeAllocRow(long windowPtr) {
return WINDOW_DATA.get(windowPtr).allocRow();
}
@Implementation(maxSdk = KITKAT_WATCH)
protected static boolean nativeSetNumColumns(int windowPtr, int columnNum) {
return nativeSetNumColumns((long) windowPtr, columnNum);
}
@Implementation(minSdk = LOLLIPOP)
protected static boolean nativeSetNumColumns(long windowPtr, int columnNum) {
return WINDOW_DATA.get(windowPtr).setNumColumns(columnNum);
}
@Implementation(maxSdk = KITKAT_WATCH)
protected static String nativeGetName(int windowPtr) {
return nativeGetName((long) windowPtr);
}
@Implementation(minSdk = LOLLIPOP)
protected static String nativeGetName(long windowPtr) {
return WINDOW_DATA.get(windowPtr).getName();
}
protected static int setData(long windowPtr, SQLiteStatement stmt) throws SQLiteException {
return WINDOW_DATA.setData(windowPtr, stmt);
}
private static Number nativeGetNumber(long windowPtr, int row, int column) {
Value value = WINDOW_DATA.get(windowPtr).value(row, column);
switch (value.type) {
case Cursor.FIELD_TYPE_NULL:
case SQLiteConstants.SQLITE_NULL:
return 0;
case Cursor.FIELD_TYPE_INTEGER:
case Cursor.FIELD_TYPE_FLOAT:
return (Number) value.value;
case Cursor.FIELD_TYPE_STRING: {
try {
return Double.parseDouble((String) value.value);
} catch (NumberFormatException e) {
return 0;
}
}
case Cursor.FIELD_TYPE_BLOB:
throw new android.database.sqlite.SQLiteException("could not convert "+value);
default:
throw new android.database.sqlite.SQLiteException("unknown type: "+value.type);
}
}
private static class Data {
private final List rows;
private final String name;
private int numColumns;
public Data(String name, int cursorWindowSize) {
this.name = name;
this.rows = new ArrayList();
}
public Value value(int rowN, int colN) {
Row row = rows.get(rowN);
if (row == null) {
throw new IllegalArgumentException("Bad row number: " + rowN + ", count: " + rows.size());
}
return row.get(colN);
}
public int numRows() {
return rows.size();
}
public boolean putValue(Value value, int rowN, int colN) {
return rows.get(rowN).set(colN, value);
}
public void fillWith(SQLiteStatement stmt) throws SQLiteException {
//Android caches results in the WindowedCursor to allow moveToPrevious() to function.
//Robolectric will have to cache the results too. In the rows list.
while (stmt.step()) {
rows.add(fillRowValues(stmt));
}
}
private static int cursorValueType(final int sqliteType) {
switch (sqliteType) {
case SQLiteConstants.SQLITE_NULL: return Cursor.FIELD_TYPE_NULL;
case SQLiteConstants.SQLITE_INTEGER: return Cursor.FIELD_TYPE_INTEGER;
case SQLiteConstants.SQLITE_FLOAT: return Cursor.FIELD_TYPE_FLOAT;
case SQLiteConstants.SQLITE_TEXT: return Cursor.FIELD_TYPE_STRING;
case SQLiteConstants.SQLITE_BLOB: return Cursor.FIELD_TYPE_BLOB;
default:
throw new IllegalArgumentException(
"Bad SQLite type " + sqliteType + ". See possible values in SQLiteConstants.");
}
}
private static Row fillRowValues(SQLiteStatement stmt) throws SQLiteException {
final int columnCount = stmt.columnCount();
Row row = new Row(columnCount);
for (int index = 0; index < columnCount; index++) {
row.set(index, new Value(stmt.columnValue(index), cursorValueType(stmt.columnType(index))));
}
return row;
}
public void clear() {
rows.clear();
}
public boolean allocRow() {
rows.add(new Row(numColumns));
return true;
}
public boolean setNumColumns(int numColumns) {
this.numColumns = numColumns;
return true;
}
public String getName() {
return name;
}
}
private static class Row {
private final List values;
public Row(int length) {
values = new ArrayList(length);
for (int i=0; i dataMap = new ConcurrentHashMap<>();
public Data get(long ptr) {
Data data = dataMap.get(ptr);
if (data == null) {
throw new IllegalArgumentException(
"Invalid window pointer: " + ptr + "; current pointers: " + dataMap.keySet());
}
return data;
}
public int setData(final long ptr, final SQLiteStatement stmt) throws SQLiteException {
Data data = get(ptr);
data.fillWith(stmt);
return data.numRows();
}
public void close(final long ptr) {
Data removed = dataMap.remove(ptr);
if (removed == null) {
throw new IllegalArgumentException(
"Bad cursor window pointer " + ptr + ". Valid pointers: " + dataMap.keySet());
}
}
public void clear(final long ptr) {
get(ptr).clear();
}
public long create(String name, int cursorWindowSize) {
long ptr = windowPtrCounter.incrementAndGet();
dataMap.put(ptr, new Data(name, cursorWindowSize));
return ptr;
}
}
// TODO: Implement these methods
// private static native int nativeCreateFromParcel(Parcel parcel);
// private static native void nativeWriteToParcel($ptrClass windowPtr, Parcel parcel);
// private static native void nativeFreeLastRow($ptrClass windowPtr);
// private static native void nativeCopyStringToBuffer($ptrClass windowPtr, int row, int column, CharArrayBuffer buffer);
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy