Home | History | Annotate | Download | only in shadows
      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   public static Number nativeCreate(String name, int cursorWindowSize) {
     27     return castNativePtr(WINDOW_DATA.create(name, cursorWindowSize));
     28   }
     29 
     30   @Implementation(maxSdk = KITKAT_WATCH)
     31   public static void nativeDispose(int windowPtr) {
     32     nativeDispose((long) windowPtr);
     33   }
     34 
     35   @Implementation(minSdk = LOLLIPOP)
     36   public static void nativeDispose(long windowPtr) {
     37     WINDOW_DATA.close(windowPtr);
     38   }
     39 
     40   @Implementation(maxSdk = KITKAT_WATCH)
     41   public static byte[] nativeGetBlob(int windowPtr, int row, int column) {
     42     return nativeGetBlob((long) windowPtr, row, column);
     43   }
     44 
     45   @Implementation(minSdk = LOLLIPOP)
     46   public 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   public static String nativeGetString(int windowPtr, int row, int column) {
     65     return nativeGetString((long) windowPtr, row, column);
     66   }
     67 
     68   @Implementation(minSdk = LOLLIPOP)
     69   public 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   public static long nativeGetLong(int windowPtr, int row, int column) {
     80     return nativeGetLong((long) windowPtr, row, column);
     81   }
     82 
     83   @Implementation(minSdk = LOLLIPOP)
     84   public static long nativeGetLong(long windowPtr, int row, int column) {
     85     return nativeGetNumber(windowPtr, row, column).longValue();
     86   }
     87 
     88   @Implementation(maxSdk = KITKAT_WATCH)
     89   public static double nativeGetDouble(int windowPtr, int row, int column) {
     90     return nativeGetDouble((long) windowPtr, row, column);
     91   }
     92 
     93   @Implementation(minSdk = LOLLIPOP)
     94   public static double nativeGetDouble(long windowPtr, int row, int column) {
     95     return nativeGetNumber(windowPtr, row, column).doubleValue();
     96   }
     97 
     98   @Implementation(maxSdk = KITKAT_WATCH)
     99   public static int nativeGetType(int windowPtr, int row, int column) {
    100     return nativeGetType((long) windowPtr, row, column);
    101   }
    102 
    103   @Implementation(minSdk = LOLLIPOP)
    104   public 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   public static void nativeClear(int windowPtr) {
    110     nativeClear((long) windowPtr);
    111   }
    112 
    113   @Implementation(minSdk = LOLLIPOP)
    114   public static void nativeClear(long windowPtr) {
    115     WINDOW_DATA.clear(windowPtr);
    116   }
    117 
    118   @Implementation(maxSdk = KITKAT_WATCH)
    119   public static int nativeGetNumRows(int windowPtr) {
    120     return nativeGetNumRows((long) windowPtr);
    121   }
    122 
    123   @Implementation(minSdk = LOLLIPOP)
    124   public static int nativeGetNumRows(long windowPtr) {
    125     return WINDOW_DATA.get(windowPtr).numRows();
    126   }
    127 
    128   @Implementation(maxSdk = KITKAT_WATCH)
    129   public 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   public 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   public 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   public 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   public 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   public 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   public 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   public 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   public static boolean nativePutNull(int windowPtr, int row, int column) {
    170     return nativePutNull((long) windowPtr, row, column);
    171   }
    172 
    173   @Implementation(minSdk = LOLLIPOP)
    174   public 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   public static boolean nativeAllocRow(int windowPtr) {
    180     return nativeAllocRow((long) windowPtr);
    181   }
    182 
    183   @Implementation(minSdk = LOLLIPOP)
    184   public static boolean nativeAllocRow(long windowPtr) {
    185     return WINDOW_DATA.get(windowPtr).allocRow();
    186   }
    187 
    188   @Implementation(maxSdk = KITKAT_WATCH)
    189   public static boolean nativeSetNumColumns(int windowPtr, int columnNum) {
    190     return nativeSetNumColumns((long) windowPtr, columnNum);
    191   }
    192 
    193   @Implementation(minSdk = LOLLIPOP)
    194   public static boolean nativeSetNumColumns(long windowPtr, int columnNum) {
    195     return WINDOW_DATA.get(windowPtr).setNumColumns(columnNum);
    196   }
    197 
    198   @Implementation(maxSdk = KITKAT_WATCH)
    199   public static String nativeGetName(int windowPtr) {
    200     return nativeGetName((long) windowPtr);
    201   }
    202 
    203   @Implementation(minSdk = LOLLIPOP)
    204   public 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