1 package org.robolectric.shadows; 2 3 import static android.os.Build.VERSION_CODES.KITKAT_WATCH; 4 import static android.os.Build.VERSION_CODES.LOLLIPOP; 5 import static java.nio.charset.StandardCharsets.UTF_8; 6 import static org.robolectric.RuntimeEnvironment.castNativePtr; 7 8 import android.database.Cursor; 9 import android.database.CursorWindow; 10 import com.almworks.sqlite4java.SQLiteConstants; 11 import com.almworks.sqlite4java.SQLiteException; 12 import com.almworks.sqlite4java.SQLiteStatement; 13 import java.util.ArrayList; 14 import java.util.List; 15 import java.util.Map; 16 import java.util.concurrent.ConcurrentHashMap; 17 import java.util.concurrent.atomic.AtomicLong; 18 import org.robolectric.annotation.Implementation; 19 import org.robolectric.annotation.Implements; 20 21 @Implements(value = CursorWindow.class) 22 public class ShadowCursorWindow { 23 private static final WindowData WINDOW_DATA = new WindowData(); 24 25 @Implementation 26 protected static Number nativeCreate(String name, int cursorWindowSize) { 27 return castNativePtr(WINDOW_DATA.create(name, cursorWindowSize)); 28 } 29 30 @Implementation(maxSdk = KITKAT_WATCH) 31 protected static void nativeDispose(int windowPtr) { 32 nativeDispose((long) windowPtr); 33 } 34 35 @Implementation(minSdk = LOLLIPOP) 36 protected static void nativeDispose(long windowPtr) { 37 WINDOW_DATA.close(windowPtr); 38 } 39 40 @Implementation(maxSdk = KITKAT_WATCH) 41 protected static byte[] nativeGetBlob(int windowPtr, int row, int column) { 42 return nativeGetBlob((long) windowPtr, row, column); 43 } 44 45 @Implementation(minSdk = LOLLIPOP) 46 protected static byte[] nativeGetBlob(long windowPtr, int row, int column) { 47 Value value = WINDOW_DATA.get(windowPtr).value(row, column); 48 49 switch (value.type) { 50 case Cursor.FIELD_TYPE_NULL: 51 return null; 52 case Cursor.FIELD_TYPE_BLOB: 53 // This matches Android's behavior, which does not match the SQLite spec 54 byte[] blob = (byte[])value.value; 55 return blob == null ? new byte[]{} : blob; 56 case Cursor.FIELD_TYPE_STRING: 57 return ((String)value.value).getBytes(UTF_8); 58 default: 59 throw new android.database.sqlite.SQLiteException("Getting blob when column is non-blob. Row " + row + ", col " + column); 60 } 61 } 62 63 @Implementation(maxSdk = KITKAT_WATCH) 64 protected static String nativeGetString(int windowPtr, int row, int column) { 65 return nativeGetString((long) windowPtr, row, column); 66 } 67 68 @Implementation(minSdk = LOLLIPOP) 69 protected static String nativeGetString(long windowPtr, int row, int column) { 70 Value val = WINDOW_DATA.get(windowPtr).value(row, column); 71 if (val.type == Cursor.FIELD_TYPE_BLOB) { 72 throw new android.database.sqlite.SQLiteException("Getting string when column is blob. Row " + row + ", col " + column); 73 } 74 Object value = val.value; 75 return value == null ? null : String.valueOf(value); 76 } 77 78 @Implementation(maxSdk = KITKAT_WATCH) 79 protected static long nativeGetLong(int windowPtr, int row, int column) { 80 return nativeGetLong((long) windowPtr, row, column); 81 } 82 83 @Implementation(minSdk = LOLLIPOP) 84 protected static long nativeGetLong(long windowPtr, int row, int column) { 85 return nativeGetNumber(windowPtr, row, column).longValue(); 86 } 87 88 @Implementation(maxSdk = KITKAT_WATCH) 89 protected static double nativeGetDouble(int windowPtr, int row, int column) { 90 return nativeGetDouble((long) windowPtr, row, column); 91 } 92 93 @Implementation(minSdk = LOLLIPOP) 94 protected static double nativeGetDouble(long windowPtr, int row, int column) { 95 return nativeGetNumber(windowPtr, row, column).doubleValue(); 96 } 97 98 @Implementation(maxSdk = KITKAT_WATCH) 99 protected static int nativeGetType(int windowPtr, int row, int column) { 100 return nativeGetType((long) windowPtr, row, column); 101 } 102 103 @Implementation(minSdk = LOLLIPOP) 104 protected static int nativeGetType(long windowPtr, int row, int column) { 105 return WINDOW_DATA.get(windowPtr).value(row, column).type; 106 } 107 108 @Implementation(maxSdk = KITKAT_WATCH) 109 protected static void nativeClear(int windowPtr) { 110 nativeClear((long) windowPtr); 111 } 112 113 @Implementation(minSdk = LOLLIPOP) 114 protected static void nativeClear(long windowPtr) { 115 WINDOW_DATA.clear(windowPtr); 116 } 117 118 @Implementation(maxSdk = KITKAT_WATCH) 119 protected static int nativeGetNumRows(int windowPtr) { 120 return nativeGetNumRows((long) windowPtr); 121 } 122 123 @Implementation(minSdk = LOLLIPOP) 124 protected static int nativeGetNumRows(long windowPtr) { 125 return WINDOW_DATA.get(windowPtr).numRows(); 126 } 127 128 @Implementation(maxSdk = KITKAT_WATCH) 129 protected static boolean nativePutBlob(int windowPtr, byte[] value, int row, int column) { 130 return nativePutBlob((long) windowPtr, value, row, column); 131 } 132 133 @Implementation(minSdk = LOLLIPOP) 134 protected static boolean nativePutBlob(long windowPtr, byte[] value, int row, int column) { 135 return WINDOW_DATA.get(windowPtr).putValue(new Value(value, Cursor.FIELD_TYPE_BLOB), row, column); 136 } 137 138 @Implementation(maxSdk = KITKAT_WATCH) 139 protected static boolean nativePutString(int windowPtr, String value, int row, int column) { 140 return nativePutString((long) windowPtr, value, row, column); 141 } 142 143 @Implementation(minSdk = LOLLIPOP) 144 protected static boolean nativePutString(long windowPtr, String value, int row, int column) { 145 return WINDOW_DATA.get(windowPtr).putValue(new Value(value, Cursor.FIELD_TYPE_STRING), row, column); 146 } 147 148 @Implementation(maxSdk = KITKAT_WATCH) 149 protected static boolean nativePutLong(int windowPtr, long value, int row, int column) { 150 return nativePutLong((long) windowPtr, value, row, column); 151 } 152 153 @Implementation(minSdk = LOLLIPOP) 154 protected static boolean nativePutLong(long windowPtr, long value, int row, int column) { 155 return WINDOW_DATA.get(windowPtr).putValue(new Value(value, Cursor.FIELD_TYPE_INTEGER), row, column); 156 } 157 158 @Implementation(maxSdk = KITKAT_WATCH) 159 protected static boolean nativePutDouble(int windowPtr, double value, int row, int column) { 160 return nativePutDouble((long) windowPtr, value, row, column); 161 } 162 163 @Implementation(minSdk = LOLLIPOP) 164 protected static boolean nativePutDouble(long windowPtr, double value, int row, int column) { 165 return WINDOW_DATA.get(windowPtr).putValue(new Value(value, Cursor.FIELD_TYPE_FLOAT), row, column); 166 } 167 168 @Implementation(maxSdk = KITKAT_WATCH) 169 protected static boolean nativePutNull(int windowPtr, int row, int column) { 170 return nativePutNull((long) windowPtr, row, column); 171 } 172 173 @Implementation(minSdk = LOLLIPOP) 174 protected static boolean nativePutNull(long windowPtr, int row, int column) { 175 return WINDOW_DATA.get(windowPtr).putValue(new Value(null, Cursor.FIELD_TYPE_NULL), row, column); 176 } 177 178 @Implementation(maxSdk = KITKAT_WATCH) 179 protected static boolean nativeAllocRow(int windowPtr) { 180 return nativeAllocRow((long) windowPtr); 181 } 182 183 @Implementation(minSdk = LOLLIPOP) 184 protected static boolean nativeAllocRow(long windowPtr) { 185 return WINDOW_DATA.get(windowPtr).allocRow(); 186 } 187 188 @Implementation(maxSdk = KITKAT_WATCH) 189 protected static boolean nativeSetNumColumns(int windowPtr, int columnNum) { 190 return nativeSetNumColumns((long) windowPtr, columnNum); 191 } 192 193 @Implementation(minSdk = LOLLIPOP) 194 protected static boolean nativeSetNumColumns(long windowPtr, int columnNum) { 195 return WINDOW_DATA.get(windowPtr).setNumColumns(columnNum); 196 } 197 198 @Implementation(maxSdk = KITKAT_WATCH) 199 protected static String nativeGetName(int windowPtr) { 200 return nativeGetName((long) windowPtr); 201 } 202 203 @Implementation(minSdk = LOLLIPOP) 204 protected static String nativeGetName(long windowPtr) { 205 return WINDOW_DATA.get(windowPtr).getName(); 206 } 207 208 protected static int setData(long windowPtr, SQLiteStatement stmt) throws SQLiteException { 209 return WINDOW_DATA.setData(windowPtr, stmt); 210 } 211 212 private static Number nativeGetNumber(long windowPtr, int row, int column) { 213 Value value = WINDOW_DATA.get(windowPtr).value(row, column); 214 switch (value.type) { 215 case Cursor.FIELD_TYPE_NULL: 216 case SQLiteConstants.SQLITE_NULL: 217 return 0; 218 case Cursor.FIELD_TYPE_INTEGER: 219 case Cursor.FIELD_TYPE_FLOAT: 220 return (Number) value.value; 221 case Cursor.FIELD_TYPE_STRING: { 222 try { 223 return Double.parseDouble((String) value.value); 224 } catch (NumberFormatException e) { 225 return 0; 226 } 227 } 228 case Cursor.FIELD_TYPE_BLOB: 229 throw new android.database.sqlite.SQLiteException("could not convert "+value); 230 default: 231 throw new android.database.sqlite.SQLiteException("unknown type: "+value.type); 232 } 233 } 234 235 private static class Data { 236 private final List<Row> rows; 237 private final String name; 238 private int numColumns; 239 240 public Data(String name, int cursorWindowSize) { 241 this.name = name; 242 this.rows = new ArrayList<Row>(cursorWindowSize); 243 } 244 245 public Value value(int rowN, int colN) { 246 Row row = rows.get(rowN); 247 if (row == null) { 248 throw new IllegalArgumentException("Bad row number: " + rowN + ", count: " + rows.size()); 249 } 250 return row.get(colN); 251 } 252 253 public int numRows() { 254 return rows.size(); 255 } 256 257 public boolean putValue(Value value, int rowN, int colN) { 258 return rows.get(rowN).set(colN, value); 259 } 260 261 public void fillWith(SQLiteStatement stmt) throws SQLiteException { 262 //Android caches results in the WindowedCursor to allow moveToPrevious() to function. 263 //Robolectric will have to cache the results too. In the rows list. 264 while (stmt.step()) { 265 rows.add(fillRowValues(stmt)); 266 } 267 } 268 269 private static int cursorValueType(final int sqliteType) { 270 switch (sqliteType) { 271 case SQLiteConstants.SQLITE_NULL: return Cursor.FIELD_TYPE_NULL; 272 case SQLiteConstants.SQLITE_INTEGER: return Cursor.FIELD_TYPE_INTEGER; 273 case SQLiteConstants.SQLITE_FLOAT: return Cursor.FIELD_TYPE_FLOAT; 274 case SQLiteConstants.SQLITE_TEXT: return Cursor.FIELD_TYPE_STRING; 275 case SQLiteConstants.SQLITE_BLOB: return Cursor.FIELD_TYPE_BLOB; 276 default: 277 throw new IllegalArgumentException("Bad SQLite type " + sqliteType + ". See possible values in SQLiteConstants."); 278 } 279 } 280 281 private static Row fillRowValues(SQLiteStatement stmt) throws SQLiteException { 282 final int columnCount = stmt.columnCount(); 283 Row row = new Row(columnCount); 284 for (int index = 0; index < columnCount; index++) { 285 row.set(index, new Value(stmt.columnValue(index), cursorValueType(stmt.columnType(index)))); 286 } 287 return row; 288 } 289 290 public void clear() { 291 rows.clear(); 292 } 293 294 public boolean allocRow() { 295 rows.add(new Row(numColumns)); 296 return true; 297 } 298 299 public boolean setNumColumns(int numColumns) { 300 this.numColumns = numColumns; 301 return true; 302 } 303 304 public String getName() { 305 return name; 306 } 307 } 308 309 private static class Row { 310 private final List<Value> values; 311 312 public Row(int length) { 313 values = new ArrayList<Value>(length); 314 for (int i=0; i<length; i++) { 315 values.add(new Value(null, Cursor.FIELD_TYPE_NULL)); 316 } 317 } 318 319 public Value get(int n) { 320 return values.get(n); 321 } 322 323 public boolean set(int colN, Value value) { 324 values.set(colN, value); 325 return true; 326 } 327 } 328 329 private static class Value { 330 private final Object value; 331 private final int type; 332 333 public Value(final Object value, final int type) { 334 this.value = value; 335 this.type = type; 336 } 337 } 338 339 private static class WindowData { 340 private final AtomicLong windowPtrCounter = new AtomicLong(0); 341 private final Map<Number, Data> dataMap = new ConcurrentHashMap<>(); 342 343 public Data get(long ptr) { 344 Data data = dataMap.get(ptr); 345 if (data == null) { 346 throw new IllegalArgumentException("Invalid window pointer: " + ptr + "; current pointers: " + dataMap.keySet()); 347 } 348 return data; 349 } 350 351 public int setData(final long ptr, final SQLiteStatement stmt) throws SQLiteException { 352 Data data = get(ptr); 353 data.fillWith(stmt); 354 return data.numRows(); 355 } 356 357 public void close(final long ptr) { 358 Data removed = dataMap.remove(ptr); 359 if (removed == null) { 360 throw new IllegalArgumentException("Bad cursor window pointer " + ptr + ". Valid pointers: " + dataMap.keySet()); 361 } 362 } 363 364 public void clear(final long ptr) { 365 get(ptr).clear(); 366 } 367 368 public long create(String name, int cursorWindowSize) { 369 long ptr = windowPtrCounter.incrementAndGet(); 370 dataMap.put(ptr, new Data(name, cursorWindowSize)); 371 return ptr; 372 } 373 } 374 375 // TODO: Implement these methods 376 // private static native int nativeCreateFromParcel(Parcel parcel); 377 // private static native void nativeWriteToParcel($ptrClass windowPtr, Parcel parcel); 378 // private static native void nativeFreeLastRow($ptrClass windowPtr); 379 // private static native void nativeCopyStringToBuffer($ptrClass windowPtr, int row, int column, CharArrayBuffer buffer); 380 } 381