Fakes for SQLite databases and taggings on views
diff --git a/src/com/xtremelabs/droidsugar/view/FakeAbstractCursor.java b/src/com/xtremelabs/droidsugar/view/FakeAbstractCursor.java
new file mode 100644
index 0000000..d5eaad2
--- /dev/null
+++ b/src/com/xtremelabs/droidsugar/view/FakeAbstractCursor.java
@@ -0,0 +1,15 @@
+package com.xtremelabs.droidsugar.view;
+
+import android.database.AbstractCursor;
+
+public class FakeAbstractCursor {
+    private AbstractCursor real;
+
+    public FakeAbstractCursor(AbstractCursor real) {
+        this.real = real;
+    }
+
+    public final boolean moveToFirst() {
+        return real.getCount() > 0;
+    }
+}
diff --git a/src/com/xtremelabs/droidsugar/view/FakeContentValues.java b/src/com/xtremelabs/droidsugar/view/FakeContentValues.java
new file mode 100644
index 0000000..27c7306
--- /dev/null
+++ b/src/com/xtremelabs/droidsugar/view/FakeContentValues.java
@@ -0,0 +1,293 @@
+/*
+ * Copyright (C) 2007 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 com.xtremelabs.droidsugar.view;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+
+import android.content.ContentValues;
+import android.util.Log;
+import com.xtremelabs.droidsugar.ProxyDelegatingHandler;
+
+@SuppressWarnings({"UnusedDeclaration"})
+public final class FakeContentValues {
+    private HashMap<String, Object> values = new HashMap<String, Object>();
+    private static final String TAG = "FakeContentValues";
+
+    public void __constructor__(ContentValues from) {
+        values = new HashMap<String, Object>(proxyFor(from).values);
+    }
+
+    private void __constructor__(HashMap<String, Object> values) {
+        this.values = values;
+    }
+
+    @Override
+    public boolean equals(Object object) {
+        if (!(object instanceof ContentValues)) {
+            return false;
+        }
+        return values.equals(proxyFor((ContentValues) object).values);
+    }
+
+    @Override
+    public int hashCode() {
+        return values.hashCode();
+    }
+
+    public void put(String key, String value) {
+        values.put(key, value);
+    }
+
+    public void putAll(ContentValues other) {
+        values.putAll(proxyFor(other).values);
+    }
+
+    private FakeContentValues proxyFor(ContentValues other) {
+        return ((FakeContentValues) ProxyDelegatingHandler.getInstance().proxyFor(other));
+    }
+
+    public void put(String key, Byte value) {
+        values.put(key, value);
+    }
+
+    public void put(String key, Short value) {
+        values.put(key, value);
+    }
+
+    public void put(String key, Integer value) {
+        values.put(key, value);
+    }
+
+    public void put(String key, Long value) {
+        values.put(key, value);
+    }
+
+    public void put(String key, Float value) {
+        values.put(key, value);
+    }
+
+    public void put(String key, Double value) {
+        values.put(key, value);
+    }
+
+    public void put(String key, Boolean value) {
+        values.put(key, value);
+    }
+
+    public void put(String key, byte[] value) {
+        values.put(key, value);
+    }
+
+    public void putNull(String key) {
+        values.put(key, null);
+    }
+
+    public int size() {
+        return values.size();
+    }
+
+    public void remove(String key) {
+        values.remove(key);
+    }
+
+    public void clear() {
+        values.clear();
+    }
+
+    public boolean containsKey(String key) {
+        return values.containsKey(key);
+    }
+
+    public Object get(String key) {
+        return values.get(key);
+    }
+
+    public String getAsString(String key) {
+        Object value = values.get(key);
+        return value != null ? value.toString() : null;
+    }
+
+    public Long getAsLong(String key) {
+        Object value = values.get(key);
+        try {
+            return value != null ? ((Number) value).longValue() : null;
+        } catch (ClassCastException e) {
+            if (value instanceof CharSequence) {
+                try {
+                    return Long.valueOf(value.toString());
+                } catch (NumberFormatException e2) {
+                    Log.e(TAG, "Cannot parse Long value for " + value + " at key " + key);
+                    return null;
+                }
+            } else {
+                Log.e(TAG, "Cannot cast value for " + key + " to a Long: " + value, e);
+                return null;
+            }
+        }
+    }
+
+    public Integer getAsInteger(String key) {
+        Object value = values.get(key);
+        try {
+            return value != null ? ((Number) value).intValue() : null;
+        } catch (ClassCastException e) {
+            if (value instanceof CharSequence) {
+                try {
+                    return Integer.valueOf(value.toString());
+                } catch (NumberFormatException e2) {
+                    Log.e(TAG, "Cannot parse Integer value for " + value + " at key " + key);
+                    return null;
+                }
+            } else {
+                Log.e(TAG, "Cannot cast value for " + key + " to a Integer: " + value, e);
+                return null;
+            }
+        }
+    }
+
+    public Short getAsShort(String key) {
+        Object value = values.get(key);
+        try {
+            return value != null ? ((Number) value).shortValue() : null;
+        } catch (ClassCastException e) {
+            if (value instanceof CharSequence) {
+                try {
+                    return Short.valueOf(value.toString());
+                } catch (NumberFormatException e2) {
+                    Log.e(TAG, "Cannot parse Short value for " + value + " at key " + key);
+                    return null;
+                }
+            } else {
+                Log.e(TAG, "Cannot cast value for " + key + " to a Short: " + value, e);
+                return null;
+            }
+        }
+    }
+
+    public Byte getAsByte(String key) {
+        Object value = values.get(key);
+        try {
+            return value != null ? ((Number) value).byteValue() : null;
+        } catch (ClassCastException e) {
+            if (value instanceof CharSequence) {
+                try {
+                    return Byte.valueOf(value.toString());
+                } catch (NumberFormatException e2) {
+                    Log.e(TAG, "Cannot parse Byte value for " + value + " at key " + key);
+                    return null;
+                }
+            } else {
+                Log.e(TAG, "Cannot cast value for " + key + " to a Byte: " + value, e);
+                return null;
+            }
+        }
+    }
+
+    public Double getAsDouble(String key) {
+        Object value = values.get(key);
+        try {
+            return value != null ? ((Number) value).doubleValue() : null;
+        } catch (ClassCastException e) {
+            if (value instanceof CharSequence) {
+                try {
+                    return Double.valueOf(value.toString());
+                } catch (NumberFormatException e2) {
+                    Log.e(TAG, "Cannot parse Double value for " + value + " at key " + key);
+                    return null;
+                }
+            } else {
+                Log.e(TAG, "Cannot cast value for " + key + " to a Double: " + value, e);
+                return null;
+            }
+        }
+    }
+
+    public Float getAsFloat(String key) {
+        Object value = values.get(key);
+        try {
+            return value != null ? ((Number) value).floatValue() : null;
+        } catch (ClassCastException e) {
+            if (value instanceof CharSequence) {
+                try {
+                    return Float.valueOf(value.toString());
+                } catch (NumberFormatException e2) {
+                    Log.e(TAG, "Cannot parse Float value for " + value + " at key " + key);
+                    return null;
+                }
+            } else {
+                Log.e(TAG, "Cannot cast value for " + key + " to a Float: " + value, e);
+                return null;
+            }
+        }
+    }
+
+    public Boolean getAsBoolean(String key) {
+        Object value = values.get(key);
+        try {
+            return (Boolean) value;
+        } catch (ClassCastException e) {
+            if (value instanceof CharSequence) {
+                return Boolean.valueOf(value.toString());
+            } else {
+                Log.e(TAG, "Cannot cast value for " + key + " to a Boolean: " + value, e);
+                return null;
+            }
+        }
+    }
+
+    public byte[] getAsByteArray(String key) {
+        Object value = values.get(key);
+        if (value instanceof byte[]) {
+            return (byte[]) value;
+        } else {
+            return null;
+        }
+    }
+
+    public Set<Map.Entry<String, Object>> valueSet() {
+        return values.entrySet();
+    }
+
+    public int describeContents() {
+        return 0;
+    }
+
+    @Deprecated
+    public void putStringArrayList(String key, ArrayList<String> value) {
+        values.put(key, value);
+    }
+
+    @SuppressWarnings("unchecked")
+    @Deprecated
+    public ArrayList<String> getStringArrayList(String key) {
+        return (ArrayList<String>) values.get(key);
+    }
+
+    @Override
+    public String toString() {
+        StringBuilder sb = new StringBuilder();
+        for (String name : values.keySet()) {
+            String value = getAsString(name);
+            if (sb.length() > 0) sb.append(" ");
+            sb.append(name + "=" + value);
+        }
+        return sb.toString();
+    }
+}
diff --git a/src/com/xtremelabs/droidsugar/view/FakeSQLiteDatabase.java b/src/com/xtremelabs/droidsugar/view/FakeSQLiteDatabase.java
new file mode 100644
index 0000000..c69f890
--- /dev/null
+++ b/src/com/xtremelabs/droidsugar/view/FakeSQLiteDatabase.java
@@ -0,0 +1,74 @@
+package com.xtremelabs.droidsugar.view;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import android.content.ContentValues;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteCursor;
+import android.database.sqlite.SQLiteDatabase;
+
+import static com.xtremelabs.droidsugar.view.FakeHelper.newInstanceOf;
+
+@SuppressWarnings({"UnusedDeclaration"})
+public class FakeSQLiteDatabase {
+
+    public static SQLiteDatabase openDatabase(String path, SQLiteDatabase.CursorFactory factory, int flags) {
+        return newInstanceOf(SQLiteDatabase.class);
+    }
+
+    Map<String, Table> tables = new HashMap<String, Table>();
+
+    public long insert(String table, String nullColumnHack, ContentValues values) {
+        Table theTable = getTable(table);
+        theTable.insert(values);
+        return -1;
+    }
+
+    public Cursor query(final String table, final String[] columns, String selection,
+                        String[] selectionArgs, String groupBy, String having,
+                        String orderBy) {
+        final Table theTable = getTable(table);
+        return new SQLiteCursor(null, null, null, null) {
+            private int currentRowNumber = 0;
+
+            @Override
+            public int getCount() {
+                return theTable.rows.size();
+            }
+
+            @Override
+            public byte[] getBlob(int columnIndex) {
+                return (byte[]) get(columnIndex);
+            }
+
+            @Override
+            public String getString(int columnIndex) {
+                return (String) get(columnIndex);
+            }
+
+            private Object get(int columnIndex) {
+                return theTable.rows.get(currentRowNumber).get(columns[columnIndex]);
+            }
+        };
+    }
+
+    private Table getTable(String tableName) {
+        Table table = tables.get(tableName);
+        if (table == null) {
+            table = new Table();
+            tables.put(tableName, table);
+        }
+        return table;
+    }
+
+    private class Table {
+        List<ContentValues> rows = new ArrayList<ContentValues>();
+
+        public void insert(ContentValues values) {
+            rows.add(values);
+        }
+    }
+}
diff --git a/src/com/xtremelabs/droidsugar/view/FakeView.java b/src/com/xtremelabs/droidsugar/view/FakeView.java
index e31aef6..5a6af1a 100644
--- a/src/com/xtremelabs/droidsugar/view/FakeView.java
+++ b/src/com/xtremelabs/droidsugar/view/FakeView.java
@@ -1,13 +1,15 @@
 package com.xtremelabs.droidsugar.view;
 
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
 import android.content.Context;
 import android.content.res.Resources;
 import android.view.View;
 import com.xtremelabs.droidsugar.ProxyDelegatingHandler;
 
-import java.util.ArrayList;
-import java.util.List;
-
 @SuppressWarnings({"ALL"})
 public class FakeView {
     private View realView;
@@ -20,6 +22,7 @@
     public boolean selected;
     private View.OnClickListener onClickListener;
     private Object tag;
+    private Map<Integer, Object> tags = new HashMap<Integer, Object>();
 
     public FakeView(View view) {
         this.realView = view;
@@ -123,4 +126,13 @@
     public void setTag(Object tag) {
         this.tag = tag;
     }
+
+    public Object getTag(int key) {
+        return tags.get(key);
+    }
+
+    public void setTag(int key, Object value) {
+        tags.put(key, value);
+    }
+
 }
diff --git a/test/com/xtremelabs/droidsugar/view/SQLiteDatabaseTest.java b/test/com/xtremelabs/droidsugar/view/SQLiteDatabaseTest.java
new file mode 100644
index 0000000..37a2f4f
--- /dev/null
+++ b/test/com/xtremelabs/droidsugar/view/SQLiteDatabaseTest.java
@@ -0,0 +1,58 @@
+package com.xtremelabs.droidsugar.view;
+
+import android.content.ContentValues;
+import android.database.AbstractCursor;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+import com.xtremelabs.droidsugar.DroidSugarAndroidTestRunner;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import static org.hamcrest.CoreMatchers.equalTo;
+import static org.junit.Assert.assertThat;
+
+@RunWith(DroidSugarAndroidTestRunner.class)
+public class SQLiteDatabaseTest {
+    private SQLiteDatabase database;
+
+    @Before
+    public void setUp() throws Exception {
+        DroidSugarAndroidTestRunner.addProxy(SQLiteDatabase.class, FakeSQLiteDatabase.class);
+        DroidSugarAndroidTestRunner.addProxy(ContentValues.class, FakeContentValues.class);
+        DroidSugarAndroidTestRunner.addProxy(AbstractCursor.class, FakeAbstractCursor.class);
+
+        database = SQLiteDatabase.openDatabase("path", null, 0);
+    }
+
+    @Test
+    public void testInsertAndQuery() throws Exception {
+
+        String stringColumnValue = "column_value";
+        byte[] byteColumnValue = new byte[] {1,2,3};
+
+        ContentValues values = new ContentValues();
+
+        values.put("first_column", stringColumnValue);
+        values.put("second_column", byteColumnValue);
+
+        database.insert("table_name", null, values);
+
+        Cursor cursor = database.query("table_name", new String[] {"second_column", "first_column"}, null, null, null, null, null);
+
+        assertThat(cursor.moveToFirst(), equalTo(true));
+
+        byte[] byteValueFromDatabase = cursor.getBlob(0);
+        String stringValueFromDatabase = cursor.getString(1);
+
+        assertThat(stringValueFromDatabase, equalTo(stringColumnValue));
+        assertThat(byteValueFromDatabase, equalTo(byteColumnValue));
+    }
+
+    @Test
+    public void testEmptyTable() throws Exception {
+        Cursor cursor = database.query("table_name", new String[] {"second_column", "first_column"}, null, null, null, null, null);
+
+        assertThat(cursor.moveToFirst(), equalTo(false));
+    }
+}