Add a SQLite mode that enables the new native Android SQLite shadows

@SQLiteMode(LEGACY) will use the sqlite4java backed shadows.
@SQLiteMode(NATIVE) will use the new set of shadows backed by native Android
code from AOSP.

Also, add a shadow picker for the sqlite shadows that picks the shadows based
on the mode.

There is some logic added to RobolectricTestRunnner that treats the non-default
SQLite shadows (currently the native shadows) as custom shadows in order to
perform invokedynamic switchpoint invalidation. This allows switching between
the shadows within the same sandbox, avoiding the need to create a new Sandbox
is the SQLite mode is changed.
diff --git a/annotations/src/main/java/org/robolectric/annotation/SQLiteMode.java b/annotations/src/main/java/org/robolectric/annotation/SQLiteMode.java
new file mode 100644
index 0000000..63f9169
--- /dev/null
+++ b/annotations/src/main/java/org/robolectric/annotation/SQLiteMode.java
@@ -0,0 +1,27 @@
+package org.robolectric.annotation;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * A {@link org.robolectric.pluginapi.config.Configurer} annotation for controlling which SQLite
+ * shadow implementation is used for the {@link android.database} package.
+ */
+@Documented
+@Retention(RetentionPolicy.RUNTIME)
+@Target({ElementType.PACKAGE, ElementType.TYPE, ElementType.METHOD})
+public @interface SQLiteMode {
+
+  /** Specifies the different supported SQLite modes. */
+  enum Mode {
+    /** Use the legacy SQLite implementation backed by sqlite4java. */
+    LEGACY,
+    /** Use the new SQLite implementation backed by native Android code from AOSP. */
+    NATIVE,
+  }
+
+  Mode value();
+}
diff --git a/nativeruntime/build.gradle b/nativeruntime/build.gradle
new file mode 100644
index 0000000..9ab0b2f
--- /dev/null
+++ b/nativeruntime/build.gradle
@@ -0,0 +1,10 @@
+import org.robolectric.gradle.DeployedRoboJavaModulePlugin
+import org.robolectric.gradle.RoboJavaModulePlugin
+
+apply plugin: RoboJavaModulePlugin
+apply plugin: DeployedRoboJavaModulePlugin
+
+dependencies {
+  api "com.google.guava:guava:27.0.1-jre"
+  compileOnly AndroidSdk.MAX_SDK.coordinates
+}
diff --git a/nativeruntime/androidfw/CursorWindow.cpp b/nativeruntime/cpp/androidfw/CursorWindow.cpp
similarity index 100%
rename from nativeruntime/androidfw/CursorWindow.cpp
rename to nativeruntime/cpp/androidfw/CursorWindow.cpp
diff --git a/nativeruntime/androidfw/include/androidfw/CursorWindow.h b/nativeruntime/cpp/androidfw/include/androidfw/CursorWindow.h
similarity index 100%
rename from nativeruntime/androidfw/include/androidfw/CursorWindow.h
rename to nativeruntime/cpp/androidfw/include/androidfw/CursorWindow.h
diff --git a/nativeruntime/base/include/android-base/macros.h b/nativeruntime/cpp/base/include/android-base/macros.h
similarity index 100%
rename from nativeruntime/base/include/android-base/macros.h
rename to nativeruntime/cpp/base/include/android-base/macros.h
diff --git a/nativeruntime/jni/AndroidRuntime.cpp b/nativeruntime/cpp/jni/AndroidRuntime.cpp
similarity index 100%
rename from nativeruntime/jni/AndroidRuntime.cpp
rename to nativeruntime/cpp/jni/AndroidRuntime.cpp
diff --git a/nativeruntime/jni/AndroidRuntime.h b/nativeruntime/cpp/jni/AndroidRuntime.h
similarity index 100%
rename from nativeruntime/jni/AndroidRuntime.h
rename to nativeruntime/cpp/jni/AndroidRuntime.h
diff --git a/nativeruntime/jni/JNIMain.cpp b/nativeruntime/cpp/jni/JNIMain.cpp
similarity index 100%
rename from nativeruntime/jni/JNIMain.cpp
rename to nativeruntime/cpp/jni/JNIMain.cpp
diff --git a/nativeruntime/jni/robo_android_database_CursorWindow.cpp b/nativeruntime/cpp/jni/robo_android_database_CursorWindow.cpp
similarity index 100%
rename from nativeruntime/jni/robo_android_database_CursorWindow.cpp
rename to nativeruntime/cpp/jni/robo_android_database_CursorWindow.cpp
diff --git a/nativeruntime/jni/robo_android_database_SQLiteCommon.cpp b/nativeruntime/cpp/jni/robo_android_database_SQLiteCommon.cpp
similarity index 100%
rename from nativeruntime/jni/robo_android_database_SQLiteCommon.cpp
rename to nativeruntime/cpp/jni/robo_android_database_SQLiteCommon.cpp
diff --git a/nativeruntime/jni/robo_android_database_SQLiteCommon.h b/nativeruntime/cpp/jni/robo_android_database_SQLiteCommon.h
similarity index 100%
rename from nativeruntime/jni/robo_android_database_SQLiteCommon.h
rename to nativeruntime/cpp/jni/robo_android_database_SQLiteCommon.h
diff --git a/nativeruntime/jni/robo_android_database_SQLiteConnection.cpp b/nativeruntime/cpp/jni/robo_android_database_SQLiteConnection.cpp
similarity index 100%
rename from nativeruntime/jni/robo_android_database_SQLiteConnection.cpp
rename to nativeruntime/cpp/jni/robo_android_database_SQLiteConnection.cpp
diff --git a/nativeruntime/libcutils/ashmem.cpp b/nativeruntime/cpp/libcutils/ashmem.cpp
similarity index 100%
rename from nativeruntime/libcutils/ashmem.cpp
rename to nativeruntime/cpp/libcutils/ashmem.cpp
diff --git a/nativeruntime/libcutils/include/cutils/ashmem.h b/nativeruntime/cpp/libcutils/include/cutils/ashmem.h
similarity index 100%
rename from nativeruntime/libcutils/include/cutils/ashmem.h
rename to nativeruntime/cpp/libcutils/include/cutils/ashmem.h
diff --git a/nativeruntime/liblog/include/log/log.h b/nativeruntime/cpp/liblog/include/log/log.h
similarity index 100%
rename from nativeruntime/liblog/include/log/log.h
rename to nativeruntime/cpp/liblog/include/log/log.h
diff --git a/nativeruntime/liblog/log.c b/nativeruntime/cpp/liblog/log.c
similarity index 100%
rename from nativeruntime/liblog/log.c
rename to nativeruntime/cpp/liblog/log.c
diff --git a/nativeruntime/libnativehelper/include/nativehelper/JNIHelp.h b/nativeruntime/cpp/libnativehelper/include/nativehelper/JNIHelp.h
similarity index 100%
rename from nativeruntime/libnativehelper/include/nativehelper/JNIHelp.h
rename to nativeruntime/cpp/libnativehelper/include/nativehelper/JNIHelp.h
diff --git a/nativeruntime/libnativehelper/include/nativehelper/scoped_local_ref.h b/nativeruntime/cpp/libnativehelper/include/nativehelper/scoped_local_ref.h
similarity index 100%
rename from nativeruntime/libnativehelper/include/nativehelper/scoped_local_ref.h
rename to nativeruntime/cpp/libnativehelper/include/nativehelper/scoped_local_ref.h
diff --git a/nativeruntime/libnativehelper/include/nativehelper/scoped_utf8_chars.h b/nativeruntime/cpp/libnativehelper/include/nativehelper/scoped_utf8_chars.h
similarity index 100%
rename from nativeruntime/libnativehelper/include/nativehelper/scoped_utf8_chars.h
rename to nativeruntime/cpp/libnativehelper/include/nativehelper/scoped_utf8_chars.h
diff --git a/nativeruntime/libutils/SharedBuffer.cpp b/nativeruntime/cpp/libutils/SharedBuffer.cpp
similarity index 100%
rename from nativeruntime/libutils/SharedBuffer.cpp
rename to nativeruntime/cpp/libutils/SharedBuffer.cpp
diff --git a/nativeruntime/libutils/SharedBuffer.h b/nativeruntime/cpp/libutils/SharedBuffer.h
similarity index 100%
rename from nativeruntime/libutils/SharedBuffer.h
rename to nativeruntime/cpp/libutils/SharedBuffer.h
diff --git a/nativeruntime/libutils/String16.cpp b/nativeruntime/cpp/libutils/String16.cpp
similarity index 100%
rename from nativeruntime/libutils/String16.cpp
rename to nativeruntime/cpp/libutils/String16.cpp
diff --git a/nativeruntime/libutils/String8.cpp b/nativeruntime/cpp/libutils/String8.cpp
similarity index 100%
rename from nativeruntime/libutils/String8.cpp
rename to nativeruntime/cpp/libutils/String8.cpp
diff --git a/nativeruntime/libutils/Unicode.cpp b/nativeruntime/cpp/libutils/Unicode.cpp
similarity index 100%
rename from nativeruntime/libutils/Unicode.cpp
rename to nativeruntime/cpp/libutils/Unicode.cpp
diff --git a/nativeruntime/libutils/include/utils/Compat.h b/nativeruntime/cpp/libutils/include/utils/Compat.h
similarity index 100%
rename from nativeruntime/libutils/include/utils/Compat.h
rename to nativeruntime/cpp/libutils/include/utils/Compat.h
diff --git a/nativeruntime/libutils/include/utils/Errors.h b/nativeruntime/cpp/libutils/include/utils/Errors.h
similarity index 100%
rename from nativeruntime/libutils/include/utils/Errors.h
rename to nativeruntime/cpp/libutils/include/utils/Errors.h
diff --git a/nativeruntime/libutils/include/utils/String16.h b/nativeruntime/cpp/libutils/include/utils/String16.h
similarity index 100%
rename from nativeruntime/libutils/include/utils/String16.h
rename to nativeruntime/cpp/libutils/include/utils/String16.h
diff --git a/nativeruntime/libutils/include/utils/String8.h b/nativeruntime/cpp/libutils/include/utils/String8.h
similarity index 100%
rename from nativeruntime/libutils/include/utils/String8.h
rename to nativeruntime/cpp/libutils/include/utils/String8.h
diff --git a/nativeruntime/libutils/include/utils/TypeHelpers.h b/nativeruntime/cpp/libutils/include/utils/TypeHelpers.h
similarity index 100%
rename from nativeruntime/libutils/include/utils/TypeHelpers.h
rename to nativeruntime/cpp/libutils/include/utils/TypeHelpers.h
diff --git a/nativeruntime/libutils/include/utils/Unicode.h b/nativeruntime/cpp/libutils/include/utils/Unicode.h
similarity index 100%
rename from nativeruntime/libutils/include/utils/Unicode.h
rename to nativeruntime/cpp/libutils/include/utils/Unicode.h
diff --git a/nativeruntime/CursorWindowNatives.java b/nativeruntime/src/main/java/org/robolectric/nativeruntime/CursorWindowNatives.java
similarity index 100%
rename from nativeruntime/CursorWindowNatives.java
rename to nativeruntime/src/main/java/org/robolectric/nativeruntime/CursorWindowNatives.java
diff --git a/nativeruntime/NativeRuntimeLoader.java b/nativeruntime/src/main/java/org/robolectric/nativeruntime/NativeRuntimeLoader.java
similarity index 100%
rename from nativeruntime/NativeRuntimeLoader.java
rename to nativeruntime/src/main/java/org/robolectric/nativeruntime/NativeRuntimeLoader.java
diff --git a/nativeruntime/SQLiteConnectionNatives.java b/nativeruntime/src/main/java/org/robolectric/nativeruntime/SQLiteConnectionNatives.java
similarity index 100%
rename from nativeruntime/SQLiteConnectionNatives.java
rename to nativeruntime/src/main/java/org/robolectric/nativeruntime/SQLiteConnectionNatives.java
diff --git a/robolectric/build.gradle b/robolectric/build.gradle
index bc456e6..67f4f9f 100755
--- a/robolectric/build.gradle
+++ b/robolectric/build.gradle
@@ -64,6 +64,9 @@
         maxParallelForks = project.maxParallelForks as int
     if (project.hasProperty('forkEvery'))
         forkEvery = project.forkEvery as int
+
+    // Skip native SQLite tests until build system is set up.
+    exclude 'org/robolectric/plugins/SQLiteModeConfigurer*'
 }
 
 project.apply plugin: CheckApiChangesPlugin
diff --git a/robolectric/src/main/java/org/robolectric/RobolectricTestRunner.java b/robolectric/src/main/java/org/robolectric/RobolectricTestRunner.java
index 550599f..a3b54ea 100644
--- a/robolectric/src/main/java/org/robolectric/RobolectricTestRunner.java
+++ b/robolectric/src/main/java/org/robolectric/RobolectricTestRunner.java
@@ -10,6 +10,7 @@
 import java.security.SecureRandom;
 import java.util.ArrayList;
 import java.util.Collection;
+import java.util.Collections;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
@@ -25,6 +26,7 @@
 import org.robolectric.annotation.Config;
 import org.robolectric.annotation.LooperMode;
 import org.robolectric.annotation.LooperMode.Mode;
+import org.robolectric.annotation.SQLiteMode;
 import org.robolectric.config.AndroidConfigurer;
 import org.robolectric.interceptors.AndroidInterceptors;
 import org.robolectric.internal.AndroidSandbox;
@@ -52,6 +54,8 @@
 import org.robolectric.pluginapi.config.ConfigurationStrategy.Configuration;
 import org.robolectric.pluginapi.config.GlobalConfigProvider;
 import org.robolectric.plugins.HierarchicalConfigurationStrategy.ConfigurationImpl;
+import org.robolectric.shadows.ShadowNativeCursorWindow;
+import org.robolectric.shadows.ShadowNativeSQLiteConnection;
 import org.robolectric.util.Logger;
 import org.robolectric.util.PerfStatsCollector;
 import org.robolectric.util.ReflectionHelpers;
@@ -527,9 +531,26 @@
   @Override
   @Nonnull
   protected Class<?>[] getExtraShadows(FrameworkMethod frameworkMethod) {
-    Config config =
-        ((RobolectricFrameworkMethod) frameworkMethod).getConfiguration().get(Config.class);
-    return config.shadows();
+    ArrayList<Class<?>> extraShadows = new ArrayList<>();
+    RobolectricFrameworkMethod roboFrameworkMethod = (RobolectricFrameworkMethod) frameworkMethod;
+    maybeAddSQLiteShadows(roboFrameworkMethod, extraShadows);
+    Config config = roboFrameworkMethod.getConfiguration().get(Config.class);
+    Collections.addAll(extraShadows, config.shadows());
+    return extraShadows.toArray(new Class<?>[] {});
+  }
+
+  /**
+   * Leverage custom shadow mechanism for shadows picked by the SQLite mode. By hooking into custom
+   * shadows, invokedynamic switchpoint invalidation can be performed, which prevents the need to
+   * create new sets of sandboxes when the SQLite mode changes.
+   */
+  private void maybeAddSQLiteShadows(
+      RobolectricFrameworkMethod roboFrameworkMethod, ArrayList<Class<?>> extraShadows) {
+    SQLiteMode.Mode sqliteMode = roboFrameworkMethod.getConfiguration().get(SQLiteMode.Mode.class);
+    if (sqliteMode == SQLiteMode.Mode.NATIVE) {
+      extraShadows.add(ShadowNativeSQLiteConnection.class);
+      extraShadows.add(ShadowNativeCursorWindow.class);
+    }
   }
 
   @Override
diff --git a/robolectric/src/main/java/org/robolectric/plugins/SQLiteModeConfigurer.java b/robolectric/src/main/java/org/robolectric/plugins/SQLiteModeConfigurer.java
new file mode 100644
index 0000000..0feae16
--- /dev/null
+++ b/robolectric/src/main/java/org/robolectric/plugins/SQLiteModeConfigurer.java
@@ -0,0 +1,65 @@
+package org.robolectric.plugins;
+
+import com.google.auto.service.AutoService;
+import java.lang.reflect.Method;
+import java.util.Properties;
+import javax.annotation.Nonnull;
+import org.robolectric.annotation.SQLiteMode;
+import org.robolectric.annotation.SQLiteMode.Mode;
+import org.robolectric.pluginapi.config.Configurer;
+
+/** Provides configuration to Robolectric for its @{@link SQLiteMode} annotation. */
+@AutoService(Configurer.class)
+public class SQLiteModeConfigurer implements Configurer<SQLiteMode.Mode> {
+
+  private final Properties systemProperties;
+
+  public SQLiteModeConfigurer(Properties systemProperties) {
+    this.systemProperties = systemProperties;
+  }
+
+  @Override
+  public Class<SQLiteMode.Mode> getConfigClass() {
+    return SQLiteMode.Mode.class;
+  }
+
+  @Nonnull
+  @Override
+  public SQLiteMode.Mode defaultConfig() {
+    return SQLiteMode.Mode.valueOf(
+        systemProperties.getProperty("robolectric.sqliteMode", "LEGACY"));
+  }
+
+  @Override
+  public SQLiteMode.Mode getConfigFor(@Nonnull String packageName) {
+    try {
+      Package pkg = Class.forName(packageName + ".package-info").getPackage();
+      return valueFrom(pkg.getAnnotation(SQLiteMode.class));
+    } catch (ClassNotFoundException e) {
+      // ignore
+    }
+    return null;
+  }
+
+  @Override
+  public SQLiteMode.Mode getConfigFor(@Nonnull Class<?> testClass) {
+    return valueFrom(testClass.getAnnotation(SQLiteMode.class));
+  }
+
+  @Override
+  public SQLiteMode.Mode getConfigFor(@Nonnull Method method) {
+    return valueFrom(method.getAnnotation(SQLiteMode.class));
+  }
+
+  @Nonnull
+  @Override
+  public SQLiteMode.Mode merge(
+      @Nonnull SQLiteMode.Mode parentConfig, @Nonnull SQLiteMode.Mode childConfig) {
+    // just take the childConfig - since SQLiteMode only has a single 'value' attribute
+    return childConfig;
+  }
+
+  private Mode valueFrom(SQLiteMode sqliteMode) {
+    return sqliteMode == null ? null : sqliteMode.value();
+  }
+}
diff --git a/robolectric/src/test/java/org/robolectric/plugins/SQLiteModeConfigurerClassTest.java b/robolectric/src/test/java/org/robolectric/plugins/SQLiteModeConfigurerClassTest.java
new file mode 100644
index 0000000..4f35d4e
--- /dev/null
+++ b/robolectric/src/test/java/org/robolectric/plugins/SQLiteModeConfigurerClassTest.java
@@ -0,0 +1,69 @@
+package org.robolectric.plugins;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.database.CursorWindow;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.annotation.Config;
+import org.robolectric.annotation.Implements;
+import org.robolectric.annotation.SQLiteMode;
+import org.robolectric.annotation.SQLiteMode.Mode;
+import org.robolectric.config.ConfigurationRegistry;
+import org.robolectric.shadow.api.Shadow;
+import org.robolectric.shadows.ShadowCursorWindow;
+import org.robolectric.shadows.ShadowLegacyCursorWindow;
+import org.robolectric.shadows.ShadowNativeCursorWindow;
+
+/** Unit tests for classes annotated with @LooperMode. */
+@RunWith(AndroidJUnit4.class)
+@SQLiteMode(Mode.LEGACY)
+public class SQLiteModeConfigurerClassTest {
+
+  @Test
+  public void defaultsToClass() {
+    assertThat(ConfigurationRegistry.get(SQLiteMode.Mode.class)).isSameInstanceAs(Mode.LEGACY);
+  }
+
+  @Test
+  @SQLiteMode(Mode.NATIVE)
+  public void overriddenAtMethod() {
+    assertThat(ConfigurationRegistry.get(Mode.class)).isSameInstanceAs(Mode.NATIVE);
+  }
+
+  @Test
+  @SQLiteMode(Mode.LEGACY)
+  public void shouldUseLegacyShadows() {
+    assertThat(ConfigurationRegistry.get(Mode.class)).isSameInstanceAs(Mode.LEGACY);
+    try (CursorWindow cursorWindow = new CursorWindow("1")) {
+      ShadowCursorWindow shadow = Shadow.extract(cursorWindow);
+      assertThat(shadow).isInstanceOf(ShadowLegacyCursorWindow.class);
+    }
+  }
+
+  @Test
+  @SQLiteMode(Mode.NATIVE)
+  public void shouldUseRealisticShadows() {
+    assertThat(ConfigurationRegistry.get(Mode.class)).isSameInstanceAs(Mode.NATIVE);
+    try (CursorWindow cursorWindow = new CursorWindow("2")) {
+      ShadowCursorWindow shadow = Shadow.extract(cursorWindow);
+      assertThat(shadow).isInstanceOf(ShadowNativeCursorWindow.class);
+    }
+  }
+
+  @Test
+  @SQLiteMode(Mode.NATIVE)
+  @Config(shadows = MyShadowCursorWindow.class)
+  public void shouldPreferCustomShadows() {
+    assertThat(ConfigurationRegistry.get(Mode.class)).isSameInstanceAs(Mode.NATIVE);
+    try (CursorWindow cursorWindow = new CursorWindow("3")) {
+      ShadowCursorWindow shadow = Shadow.extract(cursorWindow);
+      assertThat(shadow).isInstanceOf(MyShadowCursorWindow.class);
+    }
+  }
+
+  /** A custom {@link android.database.CursorWindow} shadow for testing */
+  @Implements(CursorWindow.class)
+  public static class MyShadowCursorWindow extends ShadowLegacyCursorWindow {}
+}
diff --git a/robolectric/src/test/java/org/robolectric/plugins/SQLiteModeConfigurerTest.java b/robolectric/src/test/java/org/robolectric/plugins/SQLiteModeConfigurerTest.java
new file mode 100644
index 0000000..f9bbb43
--- /dev/null
+++ b/robolectric/src/test/java/org/robolectric/plugins/SQLiteModeConfigurerTest.java
@@ -0,0 +1,28 @@
+package org.robolectric.plugins;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import java.util.Properties;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.robolectric.annotation.SQLiteMode;
+import org.robolectric.annotation.SQLiteMode.Mode;
+
+/** Unit tests for methods annotated with @{@link SQLiteMode}. */
+@RunWith(JUnit4.class)
+public class SQLiteModeConfigurerTest {
+
+  @Test
+  public void defaultConfig() {
+    Properties systemProperties = new Properties();
+    SQLiteModeConfigurer configurer = new SQLiteModeConfigurer(systemProperties);
+    assertThat(configurer.defaultConfig()).isSameInstanceAs(Mode.LEGACY);
+
+    systemProperties.setProperty("robolectric.sqliteMode", "LEGACY");
+    assertThat(configurer.defaultConfig()).isSameInstanceAs(Mode.LEGACY);
+
+    systemProperties.setProperty("robolectric.sqliteMode", "NATIVE");
+    assertThat(configurer.defaultConfig()).isSameInstanceAs(Mode.NATIVE);
+  }
+}
diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowSQLiteConnectionTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowSQLiteConnectionTest.java
index 1f677a7..e63221a 100644
--- a/robolectric/src/test/java/org/robolectric/shadows/ShadowSQLiteConnectionTest.java
+++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowSQLiteConnectionTest.java
@@ -4,7 +4,7 @@
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.common.truth.Truth.assertWithMessage;
 import static org.junit.Assert.fail;
-import static org.robolectric.shadows.ShadowSQLiteConnection.convertSQLWithLocalizedUnicodeCollator;
+import static org.robolectric.shadows.ShadowLegacySQLiteConnection.convertSQLWithLocalizedUnicodeCollator;
 
 import android.content.ContentValues;
 import android.database.Cursor;
@@ -33,7 +33,7 @@
   private File databasePath;
   private long ptr;
   private SQLiteConnection conn;
-  private ShadowSQLiteConnection.Connections connections;
+  private ShadowLegacySQLiteConnection.Connections connections;
 
   @Before
   public void setUp() throws Exception {
@@ -109,13 +109,13 @@
 
   @Test
   public void nativeClose_closesConnection() {
-    ShadowSQLiteConnection.nativeClose(ptr);
+    ShadowLegacySQLiteConnection.nativeClose(ptr);
     assertWithMessage("open").that(conn.isOpen()).isFalse();
   }
 
   @Test
   public void reset_closesConnection() {
-    ShadowSQLiteConnection.reset();
+    ShadowLegacySQLiteConnection.reset();
     assertWithMessage("open").that(conn.isOpen()).isFalse();
   }
 
@@ -125,7 +125,7 @@
         ReflectionHelpers.getField(connections, "connectionsMap");
 
     assertWithMessage("connections before").that(connectionsMap).isNotEmpty();
-    ShadowSQLiteConnection.reset();
+    ShadowLegacySQLiteConnection.reset();
 
     assertWithMessage("connections after").that(connectionsMap).isEmpty();
   }
@@ -136,7 +136,7 @@
         ReflectionHelpers.getField(connections, "statementsMap");
 
     assertWithMessage("statements before").that(statementsMap).isNotEmpty();
-    ShadowSQLiteConnection.reset();
+    ShadowLegacySQLiteConnection.reset();
 
     assertWithMessage("statements after").that(statementsMap).isEmpty();
   }
@@ -165,7 +165,7 @@
     } finally {
       Thread.interrupted();
     }
-    ShadowSQLiteConnection.reset();
+    ShadowLegacySQLiteConnection.reset();
   }
 
   @Test
@@ -184,7 +184,7 @@
         database.compileStatement("insert into routine(name) values ('Hand press 1')");
     SQLiteStatement statement2 =
         database.compileStatement("insert into routine(name) values ('Hand press 2')");
-    ShadowSQLiteConnection.nativeCancel(ptr);
+    ShadowLegacySQLiteConnection.nativeCancel(ptr);
     // An attempt to execute a statement after a cancellation should be a no-op, unless the
     // statement hasn't been cancelled, in which case it will throw a SQLiteInterruptedException.
     statement1.execute();
@@ -199,10 +199,11 @@
 
   private SQLiteConnection getSQLiteConnection() {
     ptr =
-        ShadowSQLiteConnection.nativeOpen(
+        ShadowLegacySQLiteConnection.nativeOpen(
                 databasePath.getPath(), 0, "test connection", false, false)
             .longValue();
-    connections = ReflectionHelpers.getStaticField(ShadowSQLiteConnection.class, "CONNECTIONS");
+    connections =
+        ReflectionHelpers.getStaticField(ShadowLegacySQLiteConnection.class, "CONNECTIONS");
     return connections.getConnection(ptr);
   }
 }
diff --git a/settings.gradle b/settings.gradle
index a08fe91..8f76b9b 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -19,6 +19,7 @@
 include ":shadows:supportv4"
 include ":shadowapi"
 include ":errorprone"
+include ":nativeruntime"
 include ":integration_tests:agp"
 include ":integration_tests:agp:testsupport"
 include ":integration_tests:dependency-on-stubs"
diff --git a/shadows/framework/build.gradle b/shadows/framework/build.gradle
index 426181b..da4f7e5 100644
--- a/shadows/framework/build.gradle
+++ b/shadows/framework/build.gradle
@@ -43,6 +43,7 @@
 
 dependencies {
     api project(":annotations")
+    api project(":nativeruntime")
     api project(":resources")
     api project(":pluginapi")
     api project(":shadowapi")
diff --git a/nativeruntime/shadows/PreLPointers.java b/shadows/framework/src/main/java/org/robolectric/shadows/PreLPointers.java
similarity index 95%
rename from nativeruntime/shadows/PreLPointers.java
rename to shadows/framework/src/main/java/org/robolectric/shadows/PreLPointers.java
index c2be2ce..46830f9 100644
--- a/nativeruntime/shadows/PreLPointers.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/PreLPointers.java
@@ -1,4 +1,4 @@
-package org.robolectric.nativeruntime.shadows;
+package org.robolectric.shadows;
 
 import java.util.Map;
 import java.util.concurrent.ConcurrentHashMap;
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/SQLiteShadowPicker.java b/shadows/framework/src/main/java/org/robolectric/shadows/SQLiteShadowPicker.java
new file mode 100644
index 0000000..53e5ed2
--- /dev/null
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/SQLiteShadowPicker.java
@@ -0,0 +1,28 @@
+package org.robolectric.shadows;
+
+import org.robolectric.annotation.SQLiteMode;
+import org.robolectric.annotation.SQLiteMode.Mode;
+import org.robolectric.config.ConfigurationRegistry;
+import org.robolectric.shadow.api.ShadowPicker;
+
+/** A {@link ShadowPicker} that selects between shadows given the SQLite mode */
+public class SQLiteShadowPicker<T> implements ShadowPicker<T> {
+
+  private final Class<? extends T> legacyShadowClass;
+  private final Class<? extends T> nativeShadowClass;
+
+  public SQLiteShadowPicker(
+      Class<? extends T> legacyShadowClass, Class<? extends T> nativeShadowClass) {
+    this.legacyShadowClass = legacyShadowClass;
+    this.nativeShadowClass = nativeShadowClass;
+  }
+
+  @Override
+  public Class<? extends T> pickShadowClass() {
+    if (ConfigurationRegistry.get(SQLiteMode.Mode.class) == Mode.NATIVE) {
+      return nativeShadowClass;
+    } else {
+      return legacyShadowClass;
+    }
+  }
+}
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowCursorWindow.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowCursorWindow.java
index abf4bab..5c6463d 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowCursorWindow.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowCursorWindow.java
@@ -1,393 +1,20 @@
 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;
 
-@Implements(value = CursorWindow.class)
+/**
+ * The base shadow class for {@link CursorWindow}.
+ *
+ * <p>The actual shadow class for {@link CursorWindow} will be selected during runtime by the
+ * Picker.
+ */
+@Implements(value = CursorWindow.class, shadowPicker = ShadowCursorWindow.Picker.class)
 public class 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);
+  /** Shadow {@link Picker} for {@link ShadowCursorWindow} */
+  public static class Picker extends SQLiteShadowPicker<ShadowCursorWindow> {
+    public Picker() {
+      super(ShadowLegacyCursorWindow.class, ShadowNativeCursorWindow.class);
     }
   }
-
-  @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<Row> rows;
-    private final String name;
-    private int numColumns;
-
-    public Data(String name, int cursorWindowSize) {
-      this.name = name;
-      this.rows = new ArrayList<Row>();
-    }
-
-    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<Value> values;
-
-    public Row(int length) {
-      values = new ArrayList<Value>(length);
-      for (int i=0; i<length; i++) {
-        values.add(new Value(null, Cursor.FIELD_TYPE_NULL));
-      }
-    }
-
-    public Value get(int n) {
-      return values.get(n);
-    }
-
-    public boolean set(int colN, Value value) {
-      values.set(colN, value);
-      return true;
-    }
-  }
-
-  private static class Value {
-    private final Object value;
-    private final int type;
-
-    public Value(final Object value, final int type) {
-      this.value = value;
-      this.type = type;
-    }
-  }
-
-  private static class WindowData {
-    private final AtomicLong windowPtrCounter = new AtomicLong(0);
-    private final Map<Number, Data> 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);
 }
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowLegacyCursorWindow.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowLegacyCursorWindow.java
new file mode 100644
index 0000000..49fcaca
--- /dev/null
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowLegacyCursorWindow.java
@@ -0,0 +1,394 @@
+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<Row> rows;
+    private final String name;
+    private int numColumns;
+
+    public Data(String name, int cursorWindowSize) {
+      this.name = name;
+      this.rows = new ArrayList<Row>();
+    }
+
+    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<Value> values;
+
+    public Row(int length) {
+      values = new ArrayList<Value>(length);
+      for (int i=0; i<length; i++) {
+        values.add(new Value(null, Cursor.FIELD_TYPE_NULL));
+      }
+    }
+
+    public Value get(int n) {
+      return values.get(n);
+    }
+
+    public boolean set(int colN, Value value) {
+      values.set(colN, value);
+      return true;
+    }
+  }
+
+  private static class Value {
+    private final Object value;
+    private final int type;
+
+    public Value(final Object value, final int type) {
+      this.value = value;
+      this.type = type;
+    }
+  }
+
+  private static class WindowData {
+    private final AtomicLong windowPtrCounter = new AtomicLong(0);
+    private final Map<Number, Data> 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);
+}
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowLegacySQLiteConnection.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowLegacySQLiteConnection.java
new file mode 100644
index 0000000..6efe920
--- /dev/null
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowLegacySQLiteConnection.java
@@ -0,0 +1,898 @@
+package org.robolectric.shadows;
+
+import static android.os.Build.VERSION_CODES.KITKAT_WATCH;
+import static android.os.Build.VERSION_CODES.LOLLIPOP;
+import static android.os.Build.VERSION_CODES.O;
+import static android.os.Build.VERSION_CODES.O_MR1;
+import static android.os.Build.VERSION_CODES.Q;
+import static org.robolectric.RuntimeEnvironment.castNativePtr;
+
+import android.database.sqlite.SQLiteAbortException;
+import android.database.sqlite.SQLiteAccessPermException;
+import android.database.sqlite.SQLiteBindOrColumnIndexOutOfRangeException;
+import android.database.sqlite.SQLiteBlobTooBigException;
+import android.database.sqlite.SQLiteCantOpenDatabaseException;
+import android.database.sqlite.SQLiteConstraintException;
+import android.database.sqlite.SQLiteCustomFunction;
+import android.database.sqlite.SQLiteDatabaseCorruptException;
+import android.database.sqlite.SQLiteDatabaseLockedException;
+import android.database.sqlite.SQLiteDatatypeMismatchException;
+import android.database.sqlite.SQLiteDiskIOException;
+import android.database.sqlite.SQLiteDoneException;
+import android.database.sqlite.SQLiteFullException;
+import android.database.sqlite.SQLiteMisuseException;
+import android.database.sqlite.SQLiteOutOfMemoryException;
+import android.database.sqlite.SQLiteReadOnlyDatabaseException;
+import android.database.sqlite.SQLiteTableLockedException;
+import android.os.OperationCanceledException;
+import com.almworks.sqlite4java.SQLiteConnection;
+import com.almworks.sqlite4java.SQLiteConstants;
+import com.almworks.sqlite4java.SQLiteException;
+import com.almworks.sqlite4java.SQLiteStatement;
+import com.google.common.util.concurrent.Uninterruptibles;
+import java.io.File;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.Callable;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.ThreadFactory;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicLong;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import org.robolectric.annotation.Implementation;
+import org.robolectric.annotation.Implements;
+import org.robolectric.annotation.Resetter;
+import org.robolectric.shadows.util.SQLiteLibraryLoader;
+import org.robolectric.util.PerfStatsCollector;
+
+/** Shadow for {@link android.database.sqlite.SQLiteConnection} that is backed by sqlite4java. */
+@Implements(value = android.database.sqlite.SQLiteConnection.class, isInAndroidSdk = false)
+public class ShadowLegacySQLiteConnection extends ShadowSQLiteConnection {
+
+  private static final String IN_MEMORY_PATH = ":memory:";
+  private static final Connections CONNECTIONS = new Connections();
+  private static final Pattern COLLATE_LOCALIZED_UNICODE_PATTERN =
+      Pattern.compile("\\s+COLLATE\\s+(LOCALIZED|UNICODE)", Pattern.CASE_INSENSITIVE);
+
+  // indicates an ignored statement
+  private static final int IGNORED_REINDEX_STMT = -2;
+
+  @Implementation(maxSdk = O)
+  protected static Number nativeOpen(
+      String path, int openFlags, String label, boolean enableTrace, boolean enableProfile) {
+    SQLiteLibraryLoader.load();
+    return castNativePtr(CONNECTIONS.open(path));
+  }
+
+  @Implementation(minSdk = O_MR1)
+  protected static long nativeOpen(
+      String path,
+      int openFlags,
+      String label,
+      boolean enableTrace,
+      boolean enableProfile,
+      int lookasideSlotSize,
+      int lookasideSlotCount) {
+    return nativeOpen(path, openFlags, label, enableTrace, enableProfile).longValue();
+  }
+
+  @Implementation(maxSdk = KITKAT_WATCH)
+  protected static int nativePrepareStatement(int connectionPtr, String sql) {
+    return (int) nativePrepareStatement((long) connectionPtr, sql);
+  }
+
+  @Implementation(minSdk = LOLLIPOP)
+  protected static long nativePrepareStatement(long connectionPtr, String sql) {
+    final String newSql = convertSQLWithLocalizedUnicodeCollator(sql);
+    return CONNECTIONS.prepareStatement(connectionPtr, newSql);
+  }
+
+  /**
+   * Convert SQL with phrase COLLATE LOCALIZED or COLLATE UNICODE to COLLATE NOCASE.
+   */
+  static String convertSQLWithLocalizedUnicodeCollator(String sql) {
+    Matcher matcher = COLLATE_LOCALIZED_UNICODE_PATTERN.matcher(sql);
+    return matcher.replaceAll(" COLLATE NOCASE");
+  }
+
+  @Resetter
+  public static void reset() {
+    CONNECTIONS.reset();
+  }
+
+  @Implementation(maxSdk = KITKAT_WATCH)
+  protected static void nativeClose(int connectionPtr) {
+    nativeClose((long) connectionPtr);
+  }
+
+  @Implementation(minSdk = LOLLIPOP)
+  protected static void nativeClose(long connectionPtr) {
+    CONNECTIONS.close(connectionPtr);
+  }
+
+  @Implementation(maxSdk = KITKAT_WATCH)
+  protected static void nativeFinalizeStatement(int connectionPtr, int statementPtr) {
+    nativeFinalizeStatement((long) connectionPtr, statementPtr);
+  }
+
+  @Implementation(minSdk = LOLLIPOP)
+  protected static void nativeFinalizeStatement(long connectionPtr, long statementPtr) {
+    CONNECTIONS.finalizeStmt(connectionPtr, statementPtr);
+  }
+
+  @Implementation(maxSdk = KITKAT_WATCH)
+  protected static int nativeGetParameterCount(int connectionPtr, int statementPtr) {
+    return nativeGetParameterCount((long) connectionPtr, statementPtr);
+  }
+
+  @Implementation(minSdk = LOLLIPOP)
+  protected static int nativeGetParameterCount(final long connectionPtr, final long statementPtr) {
+    return CONNECTIONS.getParameterCount(connectionPtr, statementPtr);
+  }
+
+  @Implementation(maxSdk = KITKAT_WATCH)
+  protected static boolean nativeIsReadOnly(int connectionPtr, int statementPtr) {
+    return nativeIsReadOnly((long) connectionPtr, (long) statementPtr);
+  }
+
+  @Implementation(minSdk = LOLLIPOP)
+  protected static boolean nativeIsReadOnly(final long connectionPtr, final long statementPtr) {
+    return CONNECTIONS.isReadOnly(connectionPtr, statementPtr);
+  }
+
+  @Implementation(maxSdk = KITKAT_WATCH)
+  protected static long nativeExecuteForLong(int connectionPtr, int statementPtr) {
+    return nativeExecuteForLong((long) connectionPtr, (long) statementPtr);
+  }
+
+  @Implementation(minSdk = LOLLIPOP)
+  protected static long nativeExecuteForLong(final long connectionPtr, final long statementPtr) {
+    return CONNECTIONS.executeForLong(connectionPtr, statementPtr);
+  }
+
+  @Implementation(maxSdk = KITKAT_WATCH)
+  protected static void nativeExecute(int connectionPtr, int statementPtr) {
+    nativeExecute((long) connectionPtr, (long) statementPtr);
+  }
+
+  @Implementation(minSdk = LOLLIPOP)
+  protected static void nativeExecute(final long connectionPtr, final long statementPtr) {
+    CONNECTIONS.executeStatement(connectionPtr, statementPtr);
+  }
+
+  @Implementation(maxSdk = KITKAT_WATCH)
+  protected static String nativeExecuteForString(int connectionPtr, int statementPtr) {
+    return nativeExecuteForString((long) connectionPtr, (long) statementPtr);
+  }
+
+  @Implementation(minSdk = LOLLIPOP)
+  protected static String nativeExecuteForString(
+      final long connectionPtr, final long statementPtr) {
+    return CONNECTIONS.executeForString(connectionPtr, statementPtr);
+  }
+
+  @Implementation(maxSdk = KITKAT_WATCH)
+  protected static int nativeGetColumnCount(int connectionPtr, int statementPtr) {
+    return nativeGetColumnCount((long) connectionPtr, (long) statementPtr);
+  }
+
+  @Implementation(minSdk = LOLLIPOP)
+  protected static int nativeGetColumnCount(final long connectionPtr, final long statementPtr) {
+    return CONNECTIONS.getColumnCount(connectionPtr, statementPtr);
+  }
+
+  @Implementation(maxSdk = KITKAT_WATCH)
+  protected static String nativeGetColumnName(int connectionPtr, int statementPtr, int index) {
+    return nativeGetColumnName((long) connectionPtr, (long) statementPtr, index);
+  }
+
+  @Implementation(minSdk = LOLLIPOP)
+  protected static String nativeGetColumnName(
+      final long connectionPtr, final long statementPtr, final int index) {
+    return CONNECTIONS.getColumnName(connectionPtr, statementPtr, index);
+  }
+
+  @Implementation(maxSdk = KITKAT_WATCH)
+  protected static void nativeBindNull(int connectionPtr, int statementPtr, int index) {
+    nativeBindNull((long) connectionPtr, (long) statementPtr, index);
+  }
+
+  @Implementation(minSdk = LOLLIPOP)
+  protected static void nativeBindNull(
+      final long connectionPtr, final long statementPtr, final int index) {
+    CONNECTIONS.bindNull(connectionPtr, statementPtr, index);
+  }
+
+  @Implementation(maxSdk = KITKAT_WATCH)
+  protected static void nativeBindLong(int connectionPtr, int statementPtr, int index, long value) {
+    nativeBindLong((long) connectionPtr, (long) statementPtr, index, value);
+  }
+
+  @Implementation(minSdk = LOLLIPOP)
+  protected static void nativeBindLong(
+      final long connectionPtr, final long statementPtr, final int index, final long value) {
+    CONNECTIONS.bindLong(connectionPtr, statementPtr, index, value);
+  }
+
+  @Implementation(maxSdk = KITKAT_WATCH)
+  protected static void nativeBindDouble(
+      int connectionPtr, int statementPtr, int index, double value) {
+    nativeBindDouble((long) connectionPtr, (long) statementPtr, index, value);
+  }
+
+  @Implementation(minSdk = LOLLIPOP)
+  protected static void nativeBindDouble(
+      final long connectionPtr, final long statementPtr, final int index, final double value) {
+    CONNECTIONS.bindDouble(connectionPtr, statementPtr, index, value);
+  }
+
+  @Implementation(maxSdk = KITKAT_WATCH)
+  protected static void nativeBindString(
+      int connectionPtr, int statementPtr, int index, String value) {
+    nativeBindString((long) connectionPtr, (long) statementPtr, index, value);
+  }
+
+  @Implementation(minSdk = LOLLIPOP)
+  protected static void nativeBindString(
+      final long connectionPtr, final long statementPtr, final int index, final String value) {
+    CONNECTIONS.bindString(connectionPtr, statementPtr, index, value);
+  }
+
+  @Implementation(maxSdk = KITKAT_WATCH)
+  protected static void nativeBindBlob(
+      int connectionPtr, int statementPtr, int index, byte[] value) {
+    nativeBindBlob((long) connectionPtr, (long) statementPtr, index, value);
+  }
+
+  @Implementation(minSdk = LOLLIPOP)
+  protected static void nativeBindBlob(
+      final long connectionPtr, final long statementPtr, final int index, final byte[] value) {
+    CONNECTIONS.bindBlob(connectionPtr, statementPtr, index, value);
+  }
+
+  @Implementation(maxSdk = KITKAT_WATCH)
+  protected static void nativeRegisterLocalizedCollators(int connectionPtr, String locale) {
+    nativeRegisterLocalizedCollators((long) connectionPtr, locale);
+  }
+
+  @Implementation(minSdk = LOLLIPOP)
+  protected static void nativeRegisterLocalizedCollators(long connectionPtr, String locale) {
+    // TODO: find a way to create a collator
+    // http://www.sqlite.org/c3ref/create_collation.html
+    // xerial jdbc driver does not have a Java method for sqlite3_create_collation
+  }
+
+  @Implementation(maxSdk = KITKAT_WATCH)
+  protected static int nativeExecuteForChangedRowCount(int connectionPtr, int statementPtr) {
+    return nativeExecuteForChangedRowCount((long) connectionPtr, (long) statementPtr);
+  }
+
+  @Implementation(minSdk = LOLLIPOP)
+  protected static int nativeExecuteForChangedRowCount(
+      final long connectionPtr, final long statementPtr) {
+    return CONNECTIONS.executeForChangedRowCount(connectionPtr, statementPtr);
+  }
+
+  @Implementation(maxSdk = KITKAT_WATCH)
+  protected static long nativeExecuteForLastInsertedRowId(int connectionPtr, int statementPtr) {
+    return nativeExecuteForLastInsertedRowId((long) connectionPtr, (long) statementPtr);
+  }
+
+  @Implementation(minSdk = LOLLIPOP)
+  protected static long nativeExecuteForLastInsertedRowId(
+      final long connectionPtr, final long statementPtr) {
+    return CONNECTIONS.executeForLastInsertedRowId(connectionPtr, statementPtr);
+  }
+
+  @Implementation(maxSdk = KITKAT_WATCH)
+  protected static long nativeExecuteForCursorWindow(
+      int connectionPtr,
+      int statementPtr,
+      int windowPtr,
+      int startPos,
+      int requiredPos,
+      boolean countAllRows) {
+    return nativeExecuteForCursorWindow((long) connectionPtr, (long) statementPtr, (long) windowPtr,
+        startPos, requiredPos, countAllRows);
+}
+
+  @Implementation(minSdk = LOLLIPOP)
+  protected static long nativeExecuteForCursorWindow(
+      final long connectionPtr,
+      final long statementPtr,
+      final long windowPtr,
+      final int startPos,
+      final int requiredPos,
+      final boolean countAllRows) {
+    return CONNECTIONS.executeForCursorWindow(connectionPtr, statementPtr, windowPtr);
+  }
+
+  @Implementation(maxSdk = KITKAT_WATCH)
+  protected static void nativeResetStatementAndClearBindings(int connectionPtr, int statementPtr) {
+    nativeResetStatementAndClearBindings((long) connectionPtr, (long) statementPtr);
+  }
+
+  @Implementation(minSdk = LOLLIPOP)
+  protected static void nativeResetStatementAndClearBindings(
+      final long connectionPtr, final long statementPtr) {
+    CONNECTIONS.resetStatementAndClearBindings(connectionPtr, statementPtr);
+  }
+
+  @Implementation(maxSdk = KITKAT_WATCH)
+  protected static void nativeCancel(int connectionPtr) {
+    nativeCancel((long) connectionPtr);
+  }
+
+  @Implementation(minSdk = LOLLIPOP)
+  protected static void nativeCancel(long connectionPtr) {
+    CONNECTIONS.cancel(connectionPtr);
+  }
+
+  @Implementation(maxSdk = KITKAT_WATCH)
+  protected static void nativeResetCancel(int connectionPtr, boolean cancelable) {
+    nativeResetCancel((long) connectionPtr, cancelable);
+  }
+
+  @Implementation(minSdk = LOLLIPOP)
+  protected static void nativeResetCancel(long connectionPtr, boolean cancelable) {
+    // handled in com.almworks.sqlite4java.SQLiteConnection#exec
+  }
+
+  @Implementation(maxSdk = KITKAT_WATCH)
+  protected static void nativeRegisterCustomFunction(
+      int connectionPtr, SQLiteCustomFunction function) {
+    nativeRegisterCustomFunction((long) connectionPtr, function);
+  }
+
+  @Implementation(minSdk = LOLLIPOP, maxSdk = Q)
+  protected static void nativeRegisterCustomFunction(
+      long connectionPtr, SQLiteCustomFunction function) {
+    // not supported
+  }
+
+  @Implementation(maxSdk = KITKAT_WATCH)
+  protected static int nativeExecuteForBlobFileDescriptor(int connectionPtr, int statementPtr) {
+    return nativeExecuteForBlobFileDescriptor((long) connectionPtr, (long) statementPtr);
+  }
+
+  @Implementation(minSdk = LOLLIPOP)
+  protected static int nativeExecuteForBlobFileDescriptor(long connectionPtr, long statementPtr) {
+    // impossible to support without native code?
+    return -1;
+  }
+
+  @Implementation(maxSdk = KITKAT_WATCH)
+  protected static int nativeGetDbLookaside(int connectionPtr) {
+    return nativeGetDbLookaside((long) connectionPtr);
+  }
+
+  @Implementation(minSdk = LOLLIPOP)
+  protected static int nativeGetDbLookaside(long connectionPtr) {
+    // not supported by sqlite4java
+    return 0;
+  }
+// VisibleForTesting
+static class Connections {
+
+  private final Object lock = new Object();
+  private final AtomicLong pointerCounter = new AtomicLong(0);
+  private final Map<Long, SQLiteStatement> statementsMap = new HashMap<>();
+  private final Map<Long, SQLiteConnection> connectionsMap = new HashMap<>();
+  private final Map<Long, List<Long>> statementPtrsForConnection = new HashMap<>();
+
+    private ExecutorService dbExecutor = Executors.newSingleThreadExecutor(threadFactory());
+
+    static ThreadFactory threadFactory() {
+      ThreadFactory delegate = Executors.defaultThreadFactory();
+      return r -> {
+        Thread worker = delegate.newThread(r);
+        worker.setName(ShadowLegacySQLiteConnection.class.getSimpleName() + " worker");
+        return worker;
+      };
+    }
+
+  SQLiteConnection getConnection(final long connectionPtr) {
+    synchronized (lock) {
+      final SQLiteConnection connection = connectionsMap.get(connectionPtr);
+      if (connection == null) {
+          throw new IllegalStateException(
+              "Illegal connection pointer "
+                  + connectionPtr
+                  + ". Current pointers for thread "
+                  + Thread.currentThread()
+                  + " "
+                  + connectionsMap.keySet());
+      }
+      return connection;
+    }
+  }
+
+  SQLiteStatement getStatement(final long connectionPtr, final long statementPtr) {
+    synchronized (lock) {
+      // ensure connection is ok
+      getConnection(connectionPtr);
+
+      final SQLiteStatement statement = statementsMap.get(statementPtr);
+      if (statement == null) {
+          throw new IllegalArgumentException(
+              "Invalid prepared statement pointer: "
+                  + statementPtr
+                  + ". Current pointers: "
+                  + statementsMap.keySet());
+      }
+      if (statement.isDisposed()) {
+          throw new IllegalStateException(
+              "Statement " + statementPtr + " " + statement + " is disposed");
+      }
+      return statement;
+    }
+  }
+
+  long open(final String path) {
+    synchronized (lock) {
+        final SQLiteConnection dbConnection =
+            execute(
+                "open SQLite connection",
+                new Callable<SQLiteConnection>() {
+                  @Override
+                  public SQLiteConnection call() throws Exception {
+                    SQLiteConnection connection =
+                        useInMemoryDatabase.get() || IN_MEMORY_PATH.equals(path)
+                            ? new SQLiteConnection()
+                            : new SQLiteConnection(new File(path));
+
+                    connection.open();
+                    return connection;
+                  }
+                });
+
+      final long connectionPtr = pointerCounter.incrementAndGet();
+      connectionsMap.put(connectionPtr, dbConnection);
+      statementPtrsForConnection.put(connectionPtr, new ArrayList<>());
+      return connectionPtr;
+    }
+  }
+
+  long prepareStatement(final long connectionPtr, final String sql) {
+    // TODO: find a way to create collators
+    if ("REINDEX LOCALIZED".equals(sql)) {
+      return IGNORED_REINDEX_STMT;
+    }
+
+    synchronized (lock) {
+      final SQLiteConnection connection = getConnection(connectionPtr);
+        final SQLiteStatement statement =
+            execute(
+                "prepare statement",
+                new Callable<SQLiteStatement>() {
+                  @Override
+                  public SQLiteStatement call() throws Exception {
+                    return connection.prepare(sql);
+                  }
+                });
+
+      final long statementPtr = pointerCounter.incrementAndGet();
+      statementsMap.put(statementPtr, statement);
+      statementPtrsForConnection.get(connectionPtr).add(statementPtr);
+      return statementPtr;
+    }
+  }
+
+  void close(final long connectionPtr) {
+    synchronized (lock) {
+      final SQLiteConnection connection = getConnection(connectionPtr);
+        execute("close connection", new Callable<Void>() {
+        @Override
+        public Void call() throws Exception {
+          connection.dispose();
+          return null;
+        }
+      });
+      connectionsMap.remove(connectionPtr);
+      statementPtrsForConnection.remove(connectionPtr);
+    }
+  }
+
+  void reset() {
+    ExecutorService oldDbExecutor;
+    Collection<SQLiteConnection> openConnections;
+
+    synchronized (lock) {
+      oldDbExecutor = dbExecutor;
+      openConnections = new ArrayList<>(connectionsMap.values());
+
+        dbExecutor = Executors.newSingleThreadExecutor(threadFactory());
+      connectionsMap.clear();
+      statementsMap.clear();
+      statementPtrsForConnection.clear();
+    }
+
+    shutdownDbExecutor(oldDbExecutor, openConnections);
+  }
+
+  private static void shutdownDbExecutor(ExecutorService executorService, Collection<SQLiteConnection> connections) {
+    for (final SQLiteConnection connection : connections) {
+      getFuture("close connection on reset", executorService.submit(new Callable<Void>() {
+        @Override
+        public Void call() throws Exception {
+          connection.dispose();
+          return null;
+        }
+      }));
+    }
+
+    executorService.shutdown();
+    try {
+      executorService.awaitTermination(30, TimeUnit.SECONDS);
+    } catch (InterruptedException e) {
+      throw new RuntimeException(e);
+    }
+  }
+
+  void finalizeStmt(final long connectionPtr, final long statementPtr) {
+    if (statementPtr == IGNORED_REINDEX_STMT) {
+      return;
+    }
+
+    synchronized (lock) {
+      final SQLiteStatement statement = getStatement(connectionPtr, statementPtr);
+      statementsMap.remove(statementPtr);
+
+        execute("finalize statement", new Callable<Void>() {
+        @Override
+        public Void call() throws Exception {
+          statement.dispose();
+          return null;
+        }
+      });
+    }
+  }
+
+  void cancel(final long connectionPtr) {
+    synchronized (lock) {
+      getConnection(connectionPtr); // check connection
+
+      for (Long statementPtr : statementPtrsForConnection.get(connectionPtr)) {
+        final SQLiteStatement statement = statementsMap.get(statementPtr);
+        if (statement != null) {
+            execute("cancel", new Callable<Void>() {
+            @Override
+            public Void call() throws Exception {
+              statement.cancel();
+              return null;
+            }
+          });
+        }
+      }
+    }
+  }
+
+  int getParameterCount(final long connectionPtr, final long statementPtr) {
+    if (statementPtr == IGNORED_REINDEX_STMT) {
+      return 0;
+    }
+
+      return executeStatementOperation(
+          connectionPtr,
+          statementPtr,
+          "get parameters count in prepared statement",
+          new StatementOperation<Integer>() {
+            @Override
+            public Integer call(final SQLiteStatement statement) throws Exception {
+              return statement.getBindParameterCount();
+            }
+          });
+  }
+
+  boolean isReadOnly(final long connectionPtr, final long statementPtr) {
+    if (statementPtr == IGNORED_REINDEX_STMT) {
+      return true;
+    }
+
+      return executeStatementOperation(
+          connectionPtr,
+          statementPtr,
+          "call isReadOnly",
+          new StatementOperation<Boolean>() {
+            @Override
+            public Boolean call(final SQLiteStatement statement) throws Exception {
+              return statement.isReadOnly();
+            }
+          });
+  }
+
+  long executeForLong(final long connectionPtr, final long statementPtr) {
+      return executeStatementOperation(
+          connectionPtr,
+          statementPtr,
+          "execute for long",
+          new StatementOperation<Long>() {
+            @Override
+            public Long call(final SQLiteStatement statement) throws Exception {
+              if (!statement.step()) {
+                throw new SQLiteException(
+                    SQLiteConstants.SQLITE_DONE, "No rows returned from query");
+              }
+              return statement.columnLong(0);
+            }
+          });
+  }
+
+  void executeStatement(final long connectionPtr, final long statementPtr) {
+    if (statementPtr == IGNORED_REINDEX_STMT) {
+      return;
+    }
+
+      executeStatementOperation(
+          connectionPtr,
+          statementPtr,
+          "execute",
+          new StatementOperation<Void>() {
+            @Override
+            public Void call(final SQLiteStatement statement) throws Exception {
+              statement.stepThrough();
+              return null;
+            }
+          });
+  }
+
+  String executeForString(final long connectionPtr, final long statementPtr) {
+      return executeStatementOperation(
+          connectionPtr,
+          statementPtr,
+          "execute for string",
+          new StatementOperation<String>() {
+            @Override
+            public String call(final SQLiteStatement statement) throws Exception {
+              if (!statement.step()) {
+                throw new SQLiteException(
+                    SQLiteConstants.SQLITE_DONE, "No rows returned from query");
+              }
+              return statement.columnString(0);
+            }
+          });
+  }
+
+  int getColumnCount(final long connectionPtr, final long statementPtr) {
+      return executeStatementOperation(
+          connectionPtr,
+          statementPtr,
+          "get columns count",
+          new StatementOperation<Integer>() {
+            @Override
+            public Integer call(final SQLiteStatement statement) throws Exception {
+              return statement.columnCount();
+            }
+          });
+  }
+
+  String getColumnName(final long connectionPtr, final long statementPtr, final int index) {
+      return executeStatementOperation(
+          connectionPtr,
+          statementPtr,
+          "get column name at index " + index,
+          new StatementOperation<String>() {
+            @Override
+            public String call(final SQLiteStatement statement) throws Exception {
+              return statement.getColumnName(index);
+            }
+          });
+  }
+
+  void bindNull(final long connectionPtr, final long statementPtr, final int index) {
+      executeStatementOperation(
+          connectionPtr,
+          statementPtr,
+          "bind null at index " + index,
+          new StatementOperation<Void>() {
+            @Override
+            public Void call(final SQLiteStatement statement) throws Exception {
+              statement.bindNull(index);
+              return null;
+            }
+          });
+  }
+
+  void bindLong(final long connectionPtr, final long statementPtr, final int index, final long value) {
+      executeStatementOperation(
+          connectionPtr,
+          statementPtr,
+          "bind long at index " + index + " with value " + value,
+          new StatementOperation<Void>() {
+            @Override
+            public Void call(final SQLiteStatement statement) throws Exception {
+              statement.bind(index, value);
+              return null;
+            }
+          });
+  }
+
+  void bindDouble(final long connectionPtr, final long statementPtr, final int index, final double value) {
+      executeStatementOperation(
+          connectionPtr,
+          statementPtr,
+          "bind double at index " + index + " with value " + value,
+          new StatementOperation<Void>() {
+            @Override
+            public Void call(final SQLiteStatement statement) throws Exception {
+              statement.bind(index, value);
+              return null;
+            }
+          });
+  }
+
+  void bindString(final long connectionPtr, final long statementPtr, final int index, final String value) {
+      executeStatementOperation(
+          connectionPtr,
+          statementPtr,
+          "bind string at index " + index,
+          new StatementOperation<Void>() {
+            @Override
+            public Void call(final SQLiteStatement statement) throws Exception {
+              statement.bind(index, value);
+              return null;
+            }
+          });
+  }
+
+  void bindBlob(final long connectionPtr, final long statementPtr, final int index, final byte[] value) {
+      executeStatementOperation(
+          connectionPtr,
+          statementPtr,
+          "bind blob at index " + index,
+          new StatementOperation<Void>() {
+            @Override
+            public Void call(final SQLiteStatement statement) throws Exception {
+              statement.bind(index, value);
+              return null;
+            }
+          });
+  }
+
+  int executeForChangedRowCount(final long connectionPtr, final long statementPtr) {
+    synchronized (lock) {
+      final SQLiteConnection connection = getConnection(connectionPtr);
+      final SQLiteStatement statement = getStatement(connectionPtr, statementPtr);
+
+        return execute(
+            "execute for changed row count",
+            new Callable<Integer>() {
+              @Override
+              public Integer call() throws Exception {
+                if (statement.step()) {
+                  throw new android.database.sqlite.SQLiteException(
+                      "Queries can be performed using SQLiteDatabase query or rawQuery methods"
+                          + " only.");
+                }
+                return connection.getChanges();
+              }
+            });
+    }
+  }
+
+  long executeForLastInsertedRowId(final long connectionPtr, final long statementPtr) {
+    synchronized (lock) {
+      final SQLiteConnection connection = getConnection(connectionPtr);
+      final SQLiteStatement statement = getStatement(connectionPtr, statementPtr);
+
+        return execute(
+            "execute for last inserted row ID",
+            new Callable<Long>() {
+              @Override
+              public Long call() throws Exception {
+                statement.stepThrough();
+                return connection.getChanges() > 0 ? connection.getLastInsertId() : -1L;
+              }
+            });
+    }
+  }
+
+  long executeForCursorWindow(final long connectionPtr, final long statementPtr, final long windowPtr) {
+      return executeStatementOperation(
+          connectionPtr,
+          statementPtr,
+          "execute for cursor window",
+          new StatementOperation<Integer>() {
+            @Override
+            public Integer call(final SQLiteStatement statement) throws Exception {
+              return ShadowLegacyCursorWindow.setData(windowPtr, statement);
+            }
+          });
+  }
+
+  void resetStatementAndClearBindings(final long connectionPtr, final long statementPtr) {
+      executeStatementOperation(
+          connectionPtr,
+          statementPtr,
+          "reset statement",
+          new StatementOperation<Void>() {
+            @Override
+            public Void call(final SQLiteStatement statement) throws Exception {
+              statement.reset(true);
+              return null;
+            }
+          });
+  }
+
+  interface StatementOperation<T> {
+    T call(final SQLiteStatement statement) throws Exception;
+  }
+
+  private <T> T executeStatementOperation(final long connectionPtr,
+                                          final long statementPtr,
+                                          final String comment,
+                                          final StatementOperation<T> statementOperation) {
+    synchronized (lock) {
+      final SQLiteStatement statement = getStatement(connectionPtr, statementPtr);
+      return execute(comment, new Callable<T>() {
+        @Override
+        public T call() throws Exception {
+          return statementOperation.call(statement);
+        }
+      });
+    }
+  }
+
+  /**
+   * Any Callable passed in to execute must not synchronize on lock, as this will result in a deadlock
+   */
+  private <T> T execute(final String comment, final Callable<T> work) {
+    synchronized (lock) {
+        return PerfStatsCollector.getInstance()
+            .measure("sqlite", () -> getFuture(comment, dbExecutor.submit(work)));
+    }
+  }
+
+  private static <T> T getFuture(final String comment, final Future<T> future) {
+    try {
+      return Uninterruptibles.getUninterruptibly(future);
+      // No need to catch cancellationexception - we never cancel these futures
+    } catch (ExecutionException e) {
+      Throwable t = e.getCause();
+      if (t instanceof SQLiteException) {
+          final RuntimeException sqlException =
+              getSqliteException("Cannot " + comment, ((SQLiteException) t).getBaseErrorCode());
+        sqlException.initCause(e);
+        throw sqlException;
+        } else if (t instanceof android.database.sqlite.SQLiteException) {
+          throw (android.database.sqlite.SQLiteException) t;
+      } else {
+        throw new RuntimeException(e);
+      }
+    }
+  }
+
+  private static RuntimeException getSqliteException(final String message, final int baseErrorCode) {
+    // Mapping is from throw_sqlite3_exception in android_database_SQLiteCommon.cpp
+    switch (baseErrorCode) {
+      case SQLiteConstants.SQLITE_ABORT: return new SQLiteAbortException(message);
+      case SQLiteConstants.SQLITE_PERM: return new SQLiteAccessPermException(message);
+      case SQLiteConstants.SQLITE_RANGE: return new SQLiteBindOrColumnIndexOutOfRangeException(message);
+      case SQLiteConstants.SQLITE_TOOBIG: return new SQLiteBlobTooBigException(message);
+      case SQLiteConstants.SQLITE_CANTOPEN: return new SQLiteCantOpenDatabaseException(message);
+      case SQLiteConstants.SQLITE_CONSTRAINT: return new SQLiteConstraintException(message);
+      case SQLiteConstants.SQLITE_NOTADB: // fall through
+      case SQLiteConstants.SQLITE_CORRUPT: return new SQLiteDatabaseCorruptException(message);
+      case SQLiteConstants.SQLITE_BUSY: return new SQLiteDatabaseLockedException(message);
+      case SQLiteConstants.SQLITE_MISMATCH: return new SQLiteDatatypeMismatchException(message);
+      case SQLiteConstants.SQLITE_IOERR: return new SQLiteDiskIOException(message);
+      case SQLiteConstants.SQLITE_DONE: return new SQLiteDoneException(message);
+      case SQLiteConstants.SQLITE_FULL: return new SQLiteFullException(message);
+      case SQLiteConstants.SQLITE_MISUSE: return new SQLiteMisuseException(message);
+      case SQLiteConstants.SQLITE_NOMEM: return new SQLiteOutOfMemoryException(message);
+      case SQLiteConstants.SQLITE_READONLY: return new SQLiteReadOnlyDatabaseException(message);
+      case SQLiteConstants.SQLITE_LOCKED: return new SQLiteTableLockedException(message);
+      case SQLiteConstants.SQLITE_INTERRUPT: return new OperationCanceledException(message);
+      default: return new android.database.sqlite.SQLiteException(message
+          + ", base error code: " + baseErrorCode);
+    }
+  }
+}
+}
diff --git a/nativeruntime/shadows/ShadowCursorWindow.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeCursorWindow.java
similarity index 97%
rename from nativeruntime/shadows/ShadowCursorWindow.java
rename to shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeCursorWindow.java
index e022e1f..8c3abfa 100644
--- a/nativeruntime/shadows/ShadowCursorWindow.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeCursorWindow.java
@@ -1,4 +1,4 @@
-package org.robolectric.nativeruntime.shadows;
+package org.robolectric.shadows;
 
 import static android.os.Build.VERSION_CODES.KITKAT_WATCH;
 import static android.os.Build.VERSION_CODES.LOLLIPOP;
@@ -12,8 +12,8 @@
 import org.robolectric.nativeruntime.CursorWindowNatives;
 
 /** Shadow for {@link CursorWindow} that is backed by native code */
-@Implements(CursorWindow.class)
-public class ShadowCursorWindow {
+@Implements(value = CursorWindow.class, isInAndroidSdk = false)
+public class ShadowNativeCursorWindow extends ShadowCursorWindow {
 
   @Implementation
   protected static Number nativeCreate(String name, int cursorWindowSize) {
diff --git a/nativeruntime/shadows/ShadowSQLiteConnection.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeSQLiteConnection.java
similarity index 98%
rename from nativeruntime/shadows/ShadowSQLiteConnection.java
rename to shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeSQLiteConnection.java
index 7a05c78..ef51205 100644
--- a/nativeruntime/shadows/ShadowSQLiteConnection.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeSQLiteConnection.java
@@ -1,4 +1,4 @@
-package org.robolectric.nativeruntime.shadows;
+package org.robolectric.shadows;
 
 import static android.os.Build.VERSION_CODES.KITKAT_WATCH;
 import static android.os.Build.VERSION_CODES.LOLLIPOP;
@@ -16,8 +16,8 @@
 import org.robolectric.util.PerfStatsCollector;
 
 /** Shadow for {@link SQLiteConnection} that is backed by native code */
-@Implements(value = SQLiteConnection.class, isInAndroidSdk = false)
-public class ShadowSQLiteConnection {
+@Implements(className = "android.database.sqlite.SQLiteConnection", isInAndroidSdk = false)
+public class ShadowNativeSQLiteConnection extends ShadowSQLiteConnection {
 
   @Implementation(maxSdk = O)
   protected static Number nativeOpen(
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowSQLiteConnection.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowSQLiteConnection.java
index 5194723..1d976ec 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowSQLiteConnection.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowSQLiteConnection.java
@@ -1,75 +1,46 @@
 package org.robolectric.shadows;
 
-import static android.os.Build.VERSION_CODES.KITKAT_WATCH;
-import static android.os.Build.VERSION_CODES.LOLLIPOP;
-import static android.os.Build.VERSION_CODES.O;
-import static android.os.Build.VERSION_CODES.O_MR1;
-import static android.os.Build.VERSION_CODES.Q;
-import static org.robolectric.RuntimeEnvironment.castNativePtr;
-
-import android.database.sqlite.SQLiteAbortException;
-import android.database.sqlite.SQLiteAccessPermException;
-import android.database.sqlite.SQLiteBindOrColumnIndexOutOfRangeException;
-import android.database.sqlite.SQLiteBlobTooBigException;
-import android.database.sqlite.SQLiteCantOpenDatabaseException;
-import android.database.sqlite.SQLiteConstraintException;
-import android.database.sqlite.SQLiteCustomFunction;
-import android.database.sqlite.SQLiteDatabaseCorruptException;
-import android.database.sqlite.SQLiteDatabaseLockedException;
-import android.database.sqlite.SQLiteDatatypeMismatchException;
-import android.database.sqlite.SQLiteDiskIOException;
-import android.database.sqlite.SQLiteDoneException;
-import android.database.sqlite.SQLiteFullException;
-import android.database.sqlite.SQLiteMisuseException;
-import android.database.sqlite.SQLiteOutOfMemoryException;
-import android.database.sqlite.SQLiteReadOnlyDatabaseException;
-import android.database.sqlite.SQLiteTableLockedException;
-import android.os.OperationCanceledException;
+import android.database.sqlite.SQLiteConnection;
 import android.os.SystemProperties;
-import com.almworks.sqlite4java.SQLiteConnection;
-import com.almworks.sqlite4java.SQLiteConstants;
-import com.almworks.sqlite4java.SQLiteException;
-import com.almworks.sqlite4java.SQLiteStatement;
-import com.google.common.util.concurrent.Uninterruptibles;
-import java.io.File;
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.concurrent.Callable;
-import java.util.concurrent.ExecutionException;
-import java.util.concurrent.ExecutorService;
-import java.util.concurrent.Executors;
-import java.util.concurrent.Future;
-import java.util.concurrent.ThreadFactory;
-import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicBoolean;
-import java.util.concurrent.atomic.AtomicLong;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
-import org.robolectric.annotation.Implementation;
 import org.robolectric.annotation.Implements;
 import org.robolectric.annotation.Resetter;
-import org.robolectric.shadows.util.SQLiteLibraryLoader;
-import org.robolectric.util.PerfStatsCollector;
+import org.robolectric.annotation.SQLiteMode;
+import org.robolectric.annotation.SQLiteMode.Mode;
+import org.robolectric.config.ConfigurationRegistry;
 
-/** Shadow for {@link android.database.sqlite.SQLiteConnection} that is backed by sqlite4java. */
-@Implements(className = "android.database.sqlite.SQLiteConnection", isInAndroidSdk = false)
+/**
+ * The base shadow class for {@link SQLiteConnection} shadow APIs.
+ *
+ * <p>The actual shadow class for {@link SQLiteConnection} will be selected during runtime by the
+ * Picker.
+ */
+@Implements(
+    className = "android.database.sqlite.SQLiteConnection",
+    isInAndroidSdk = false,
+    shadowPicker = ShadowSQLiteConnection.Picker.class)
 public class ShadowSQLiteConnection {
 
-  private static final String IN_MEMORY_PATH = ":memory:";
-  private static final Connections CONNECTIONS = new Connections();
-  private static final Pattern COLLATE_LOCALIZED_UNICODE_PATTERN =
-      Pattern.compile("\\s+COLLATE\\s+(LOCALIZED|UNICODE)", Pattern.CASE_INSENSITIVE);
+  protected static AtomicBoolean useInMemoryDatabase = new AtomicBoolean();
 
-  // indicates an ignored statement
-  private static final int IGNORED_REINDEX_STMT = -2;
-
-  private static AtomicBoolean useInMemoryDatabase = new AtomicBoolean();
+  /** Shadow {@link Picker} for {@link ShadowSQLiteConnection} */
+  public static class Picker extends SQLiteShadowPicker<ShadowSQLiteConnection> {
+    public Picker() {
+      super(ShadowLegacySQLiteConnection.class, ShadowNativeSQLiteConnection.class);
+    }
+  }
 
   public static void setUseInMemoryDatabase(boolean value) {
-    useInMemoryDatabase.set(value);
+    if (sqliteMode() == Mode.LEGACY) {
+      useInMemoryDatabase.set(value);
+    } else {
+      throw new UnsupportedOperationException(
+          "this action is not supported in " + sqliteMode() + " mode.");
+    }
+  }
+
+  public static SQLiteMode.Mode sqliteMode() {
+    return ConfigurationRegistry.get(SQLiteMode.Mode.class);
   }
 
   /**
@@ -111,836 +82,8 @@
     SystemProperties.set("debug.sqlite.journalmode", value);
   }
 
-  @Implementation(maxSdk = O)
-  protected static Number nativeOpen(
-      String path, int openFlags, String label, boolean enableTrace, boolean enableProfile) {
-    SQLiteLibraryLoader.load();
-    return castNativePtr(CONNECTIONS.open(path));
-  }
-
-  @Implementation(minSdk = O_MR1)
-  protected static long nativeOpen(
-      String path,
-      int openFlags,
-      String label,
-      boolean enableTrace,
-      boolean enableProfile,
-      int lookasideSlotSize,
-      int lookasideSlotCount) {
-    return nativeOpen(path, openFlags, label, enableTrace, enableProfile).longValue();
-  }
-
-  @Implementation(maxSdk = KITKAT_WATCH)
-  protected static int nativePrepareStatement(int connectionPtr, String sql) {
-    return (int) nativePrepareStatement((long) connectionPtr, sql);
-  }
-
-  @Implementation(minSdk = LOLLIPOP)
-  protected static long nativePrepareStatement(long connectionPtr, String sql) {
-    final String newSql = convertSQLWithLocalizedUnicodeCollator(sql);
-    return CONNECTIONS.prepareStatement(connectionPtr, newSql);
-  }
-
-  /**
-   * Convert SQL with phrase COLLATE LOCALIZED or COLLATE UNICODE to COLLATE NOCASE.
-   */
-  static String convertSQLWithLocalizedUnicodeCollator(String sql) {
-    Matcher matcher = COLLATE_LOCALIZED_UNICODE_PATTERN.matcher(sql);
-    return matcher.replaceAll(" COLLATE NOCASE");
-  }
-
   @Resetter
   public static void reset() {
-    CONNECTIONS.reset();
     useInMemoryDatabase.set(false);
   }
-
-  @Implementation(maxSdk = KITKAT_WATCH)
-  protected static void nativeClose(int connectionPtr) {
-    nativeClose((long) connectionPtr);
-  }
-
-  @Implementation(minSdk = LOLLIPOP)
-  protected static void nativeClose(long connectionPtr) {
-    CONNECTIONS.close(connectionPtr);
-  }
-
-  @Implementation(maxSdk = KITKAT_WATCH)
-  protected static void nativeFinalizeStatement(int connectionPtr, int statementPtr) {
-    nativeFinalizeStatement((long) connectionPtr, statementPtr);
-  }
-
-  @Implementation(minSdk = LOLLIPOP)
-  protected static void nativeFinalizeStatement(long connectionPtr, long statementPtr) {
-    CONNECTIONS.finalizeStmt(connectionPtr, statementPtr);
-  }
-
-  @Implementation(maxSdk = KITKAT_WATCH)
-  protected static int nativeGetParameterCount(int connectionPtr, int statementPtr) {
-    return nativeGetParameterCount((long) connectionPtr, statementPtr);
-  }
-
-  @Implementation(minSdk = LOLLIPOP)
-  protected static int nativeGetParameterCount(final long connectionPtr, final long statementPtr) {
-    return CONNECTIONS.getParameterCount(connectionPtr, statementPtr);
-  }
-
-  @Implementation(maxSdk = KITKAT_WATCH)
-  protected static boolean nativeIsReadOnly(int connectionPtr, int statementPtr) {
-    return nativeIsReadOnly((long) connectionPtr, (long) statementPtr);
-  }
-
-  @Implementation(minSdk = LOLLIPOP)
-  protected static boolean nativeIsReadOnly(final long connectionPtr, final long statementPtr) {
-    return CONNECTIONS.isReadOnly(connectionPtr, statementPtr);
-  }
-
-  @Implementation(maxSdk = KITKAT_WATCH)
-  protected static long nativeExecuteForLong(int connectionPtr, int statementPtr) {
-    return nativeExecuteForLong((long) connectionPtr, (long) statementPtr);
-  }
-
-  @Implementation(minSdk = LOLLIPOP)
-  protected static long nativeExecuteForLong(final long connectionPtr, final long statementPtr) {
-    return CONNECTIONS.executeForLong(connectionPtr, statementPtr);
-  }
-
-  @Implementation(maxSdk = KITKAT_WATCH)
-  protected static void nativeExecute(int connectionPtr, int statementPtr) {
-    nativeExecute((long) connectionPtr, (long) statementPtr);
-  }
-
-  @Implementation(minSdk = LOLLIPOP)
-  protected static void nativeExecute(final long connectionPtr, final long statementPtr) {
-    CONNECTIONS.executeStatement(connectionPtr, statementPtr);
-  }
-
-  @Implementation(maxSdk = KITKAT_WATCH)
-  protected static String nativeExecuteForString(int connectionPtr, int statementPtr) {
-    return nativeExecuteForString((long) connectionPtr, (long) statementPtr);
-  }
-
-  @Implementation(minSdk = LOLLIPOP)
-  protected static String nativeExecuteForString(
-      final long connectionPtr, final long statementPtr) {
-    return CONNECTIONS.executeForString(connectionPtr, statementPtr);
-  }
-
-  @Implementation(maxSdk = KITKAT_WATCH)
-  protected static int nativeGetColumnCount(int connectionPtr, int statementPtr) {
-    return nativeGetColumnCount((long) connectionPtr, (long) statementPtr);
-  }
-
-  @Implementation(minSdk = LOLLIPOP)
-  protected static int nativeGetColumnCount(final long connectionPtr, final long statementPtr) {
-    return CONNECTIONS.getColumnCount(connectionPtr, statementPtr);
-  }
-
-  @Implementation(maxSdk = KITKAT_WATCH)
-  protected static String nativeGetColumnName(int connectionPtr, int statementPtr, int index) {
-    return nativeGetColumnName((long) connectionPtr, (long) statementPtr, index);
-  }
-
-  @Implementation(minSdk = LOLLIPOP)
-  protected static String nativeGetColumnName(
-      final long connectionPtr, final long statementPtr, final int index) {
-    return CONNECTIONS.getColumnName(connectionPtr, statementPtr, index);
-  }
-
-  @Implementation(maxSdk = KITKAT_WATCH)
-  protected static void nativeBindNull(int connectionPtr, int statementPtr, int index) {
-    nativeBindNull((long) connectionPtr, (long) statementPtr, index);
-  }
-
-  @Implementation(minSdk = LOLLIPOP)
-  protected static void nativeBindNull(
-      final long connectionPtr, final long statementPtr, final int index) {
-    CONNECTIONS.bindNull(connectionPtr, statementPtr, index);
-  }
-
-  @Implementation(maxSdk = KITKAT_WATCH)
-  protected static void nativeBindLong(int connectionPtr, int statementPtr, int index, long value) {
-    nativeBindLong((long) connectionPtr, (long) statementPtr, index, value);
-  }
-
-  @Implementation(minSdk = LOLLIPOP)
-  protected static void nativeBindLong(
-      final long connectionPtr, final long statementPtr, final int index, final long value) {
-    CONNECTIONS.bindLong(connectionPtr, statementPtr, index, value);
-  }
-
-  @Implementation(maxSdk = KITKAT_WATCH)
-  protected static void nativeBindDouble(
-      int connectionPtr, int statementPtr, int index, double value) {
-    nativeBindDouble((long) connectionPtr, (long) statementPtr, index, value);
-  }
-
-  @Implementation(minSdk = LOLLIPOP)
-  protected static void nativeBindDouble(
-      final long connectionPtr, final long statementPtr, final int index, final double value) {
-    CONNECTIONS.bindDouble(connectionPtr, statementPtr, index, value);
-  }
-
-  @Implementation(maxSdk = KITKAT_WATCH)
-  protected static void nativeBindString(
-      int connectionPtr, int statementPtr, int index, String value) {
-    nativeBindString((long) connectionPtr, (long) statementPtr, index, value);
-  }
-
-  @Implementation(minSdk = LOLLIPOP)
-  protected static void nativeBindString(
-      final long connectionPtr, final long statementPtr, final int index, final String value) {
-    CONNECTIONS.bindString(connectionPtr, statementPtr, index, value);
-  }
-
-  @Implementation(maxSdk = KITKAT_WATCH)
-  protected static void nativeBindBlob(
-      int connectionPtr, int statementPtr, int index, byte[] value) {
-    nativeBindBlob((long) connectionPtr, (long) statementPtr, index, value);
-  }
-
-  @Implementation(minSdk = LOLLIPOP)
-  protected static void nativeBindBlob(
-      final long connectionPtr, final long statementPtr, final int index, final byte[] value) {
-    CONNECTIONS.bindBlob(connectionPtr, statementPtr, index, value);
-  }
-
-  @Implementation(maxSdk = KITKAT_WATCH)
-  protected static void nativeRegisterLocalizedCollators(int connectionPtr, String locale) {
-    nativeRegisterLocalizedCollators((long) connectionPtr, locale);
-  }
-
-  @Implementation(minSdk = LOLLIPOP)
-  protected static void nativeRegisterLocalizedCollators(long connectionPtr, String locale) {
-    // TODO: find a way to create a collator
-    // http://www.sqlite.org/c3ref/create_collation.html
-    // xerial jdbc driver does not have a Java method for sqlite3_create_collation
-  }
-
-  @Implementation(maxSdk = KITKAT_WATCH)
-  protected static int nativeExecuteForChangedRowCount(int connectionPtr, int statementPtr) {
-    return nativeExecuteForChangedRowCount((long) connectionPtr, (long) statementPtr);
-  }
-
-  @Implementation(minSdk = LOLLIPOP)
-  protected static int nativeExecuteForChangedRowCount(
-      final long connectionPtr, final long statementPtr) {
-    return CONNECTIONS.executeForChangedRowCount(connectionPtr, statementPtr);
-  }
-
-  @Implementation(maxSdk = KITKAT_WATCH)
-  protected static long nativeExecuteForLastInsertedRowId(int connectionPtr, int statementPtr) {
-    return nativeExecuteForLastInsertedRowId((long) connectionPtr, (long) statementPtr);
-  }
-
-  @Implementation(minSdk = LOLLIPOP)
-  protected static long nativeExecuteForLastInsertedRowId(
-      final long connectionPtr, final long statementPtr) {
-    return CONNECTIONS.executeForLastInsertedRowId(connectionPtr, statementPtr);
-  }
-
-  @Implementation(maxSdk = KITKAT_WATCH)
-  protected static long nativeExecuteForCursorWindow(
-      int connectionPtr,
-      int statementPtr,
-      int windowPtr,
-      int startPos,
-      int requiredPos,
-      boolean countAllRows) {
-    return nativeExecuteForCursorWindow((long) connectionPtr, (long) statementPtr, (long) windowPtr,
-        startPos, requiredPos, countAllRows);
-}
-
-  @Implementation(minSdk = LOLLIPOP)
-  protected static long nativeExecuteForCursorWindow(
-      final long connectionPtr,
-      final long statementPtr,
-      final long windowPtr,
-      final int startPos,
-      final int requiredPos,
-      final boolean countAllRows) {
-    return CONNECTIONS.executeForCursorWindow(connectionPtr, statementPtr, windowPtr);
-  }
-
-  @Implementation(maxSdk = KITKAT_WATCH)
-  protected static void nativeResetStatementAndClearBindings(int connectionPtr, int statementPtr) {
-    nativeResetStatementAndClearBindings((long) connectionPtr, (long) statementPtr);
-  }
-
-  @Implementation(minSdk = LOLLIPOP)
-  protected static void nativeResetStatementAndClearBindings(
-      final long connectionPtr, final long statementPtr) {
-    CONNECTIONS.resetStatementAndClearBindings(connectionPtr, statementPtr);
-  }
-
-  @Implementation(maxSdk = KITKAT_WATCH)
-  protected static void nativeCancel(int connectionPtr) {
-    nativeCancel((long) connectionPtr);
-  }
-
-  @Implementation(minSdk = LOLLIPOP)
-  protected static void nativeCancel(long connectionPtr) {
-    CONNECTIONS.cancel(connectionPtr);
-  }
-
-  @Implementation(maxSdk = KITKAT_WATCH)
-  protected static void nativeResetCancel(int connectionPtr, boolean cancelable) {
-    nativeResetCancel((long) connectionPtr, cancelable);
-  }
-
-  @Implementation(minSdk = LOLLIPOP)
-  protected static void nativeResetCancel(long connectionPtr, boolean cancelable) {
-    // handled in com.almworks.sqlite4java.SQLiteConnection#exec
-  }
-
-  @Implementation(maxSdk = KITKAT_WATCH)
-  protected static void nativeRegisterCustomFunction(
-      int connectionPtr, SQLiteCustomFunction function) {
-    nativeRegisterCustomFunction((long) connectionPtr, function);
-  }
-
-  @Implementation(minSdk = LOLLIPOP, maxSdk = Q)
-  protected static void nativeRegisterCustomFunction(
-      long connectionPtr, SQLiteCustomFunction function) {
-    // not supported
-  }
-
-  @Implementation(maxSdk = KITKAT_WATCH)
-  protected static int nativeExecuteForBlobFileDescriptor(int connectionPtr, int statementPtr) {
-    return nativeExecuteForBlobFileDescriptor((long) connectionPtr, (long) statementPtr);
-  }
-
-  @Implementation(minSdk = LOLLIPOP)
-  protected static int nativeExecuteForBlobFileDescriptor(long connectionPtr, long statementPtr) {
-    // impossible to support without native code?
-    return -1;
-  }
-
-  @Implementation(maxSdk = KITKAT_WATCH)
-  protected static int nativeGetDbLookaside(int connectionPtr) {
-    return nativeGetDbLookaside((long) connectionPtr);
-  }
-
-  @Implementation(minSdk = LOLLIPOP)
-  protected static int nativeGetDbLookaside(long connectionPtr) {
-    // not supported by sqlite4java
-    return 0;
-  }
-// VisibleForTesting
-static class Connections {
-
-  private final Object lock = new Object();
-  private final AtomicLong pointerCounter = new AtomicLong(0);
-  private final Map<Long, SQLiteStatement> statementsMap = new HashMap<>();
-  private final Map<Long, SQLiteConnection> connectionsMap = new HashMap<>();
-  private final Map<Long, List<Long>> statementPtrsForConnection = new HashMap<>();
-
-    private ExecutorService dbExecutor = Executors.newSingleThreadExecutor(threadFactory());
-
-    static ThreadFactory threadFactory() {
-      ThreadFactory delegate = Executors.defaultThreadFactory();
-      return r -> {
-        Thread worker = delegate.newThread(r);
-        worker.setName(ShadowSQLiteConnection.class.getSimpleName() + " worker");
-        return worker;
-      };
-    }
-
-  SQLiteConnection getConnection(final long connectionPtr) {
-    synchronized (lock) {
-      final SQLiteConnection connection = connectionsMap.get(connectionPtr);
-      if (connection == null) {
-          throw new IllegalStateException(
-              "Illegal connection pointer "
-                  + connectionPtr
-                  + ". Current pointers for thread "
-                  + Thread.currentThread()
-                  + " "
-                  + connectionsMap.keySet());
-      }
-      return connection;
-    }
-  }
-
-  SQLiteStatement getStatement(final long connectionPtr, final long statementPtr) {
-    synchronized (lock) {
-      // ensure connection is ok
-      getConnection(connectionPtr);
-
-      final SQLiteStatement statement = statementsMap.get(statementPtr);
-      if (statement == null) {
-          throw new IllegalArgumentException(
-              "Invalid prepared statement pointer: "
-                  + statementPtr
-                  + ". Current pointers: "
-                  + statementsMap.keySet());
-      }
-      if (statement.isDisposed()) {
-          throw new IllegalStateException(
-              "Statement " + statementPtr + " " + statement + " is disposed");
-      }
-      return statement;
-    }
-  }
-
-  long open(final String path) {
-    synchronized (lock) {
-        final SQLiteConnection dbConnection =
-            execute(
-                "open SQLite connection",
-                new Callable<SQLiteConnection>() {
-                  @Override
-                  public SQLiteConnection call() throws Exception {
-                    SQLiteConnection connection =
-                        useInMemoryDatabase.get() || IN_MEMORY_PATH.equals(path)
-                            ? new SQLiteConnection()
-                            : new SQLiteConnection(new File(path));
-
-                    connection.open();
-                    return connection;
-                  }
-                });
-
-      final long connectionPtr = pointerCounter.incrementAndGet();
-      connectionsMap.put(connectionPtr, dbConnection);
-      statementPtrsForConnection.put(connectionPtr, new ArrayList<>());
-      return connectionPtr;
-    }
-  }
-
-  long prepareStatement(final long connectionPtr, final String sql) {
-    // TODO: find a way to create collators
-    if ("REINDEX LOCALIZED".equals(sql)) {
-      return IGNORED_REINDEX_STMT;
-    }
-
-    synchronized (lock) {
-      final SQLiteConnection connection = getConnection(connectionPtr);
-        final SQLiteStatement statement =
-            execute(
-                "prepare statement",
-                new Callable<SQLiteStatement>() {
-                  @Override
-                  public SQLiteStatement call() throws Exception {
-                    return connection.prepare(sql);
-                  }
-                });
-
-      final long statementPtr = pointerCounter.incrementAndGet();
-      statementsMap.put(statementPtr, statement);
-      statementPtrsForConnection.get(connectionPtr).add(statementPtr);
-      return statementPtr;
-    }
-  }
-
-  void close(final long connectionPtr) {
-    synchronized (lock) {
-      final SQLiteConnection connection = getConnection(connectionPtr);
-        execute("close connection", new Callable<Void>() {
-        @Override
-        public Void call() throws Exception {
-          connection.dispose();
-          return null;
-        }
-      });
-      connectionsMap.remove(connectionPtr);
-      statementPtrsForConnection.remove(connectionPtr);
-    }
-  }
-
-  void reset() {
-    ExecutorService oldDbExecutor;
-    Collection<SQLiteConnection> openConnections;
-
-    synchronized (lock) {
-      oldDbExecutor = dbExecutor;
-      openConnections = new ArrayList<>(connectionsMap.values());
-
-        dbExecutor = Executors.newSingleThreadExecutor(threadFactory());
-      connectionsMap.clear();
-      statementsMap.clear();
-      statementPtrsForConnection.clear();
-    }
-
-    shutdownDbExecutor(oldDbExecutor, openConnections);
-  }
-
-  private static void shutdownDbExecutor(ExecutorService executorService, Collection<SQLiteConnection> connections) {
-    for (final SQLiteConnection connection : connections) {
-      getFuture("close connection on reset", executorService.submit(new Callable<Void>() {
-        @Override
-        public Void call() throws Exception {
-          connection.dispose();
-          return null;
-        }
-      }));
-    }
-
-    executorService.shutdown();
-    try {
-      executorService.awaitTermination(30, TimeUnit.SECONDS);
-    } catch (InterruptedException e) {
-      throw new RuntimeException(e);
-    }
-  }
-
-  void finalizeStmt(final long connectionPtr, final long statementPtr) {
-    if (statementPtr == IGNORED_REINDEX_STMT) {
-      return;
-    }
-
-    synchronized (lock) {
-      final SQLiteStatement statement = getStatement(connectionPtr, statementPtr);
-      statementsMap.remove(statementPtr);
-
-        execute("finalize statement", new Callable<Void>() {
-        @Override
-        public Void call() throws Exception {
-          statement.dispose();
-          return null;
-        }
-      });
-    }
-  }
-
-  void cancel(final long connectionPtr) {
-    synchronized (lock) {
-      getConnection(connectionPtr); // check connection
-
-      for (Long statementPtr : statementPtrsForConnection.get(connectionPtr)) {
-        final SQLiteStatement statement = statementsMap.get(statementPtr);
-        if (statement != null) {
-            execute("cancel", new Callable<Void>() {
-            @Override
-            public Void call() throws Exception {
-              statement.cancel();
-              return null;
-            }
-          });
-        }
-      }
-    }
-  }
-
-  int getParameterCount(final long connectionPtr, final long statementPtr) {
-    if (statementPtr == IGNORED_REINDEX_STMT) {
-      return 0;
-    }
-
-      return executeStatementOperation(
-          connectionPtr,
-          statementPtr,
-          "get parameters count in prepared statement",
-          new StatementOperation<Integer>() {
-            @Override
-            public Integer call(final SQLiteStatement statement) throws Exception {
-              return statement.getBindParameterCount();
-            }
-          });
-  }
-
-  boolean isReadOnly(final long connectionPtr, final long statementPtr) {
-    if (statementPtr == IGNORED_REINDEX_STMT) {
-      return true;
-    }
-
-      return executeStatementOperation(
-          connectionPtr,
-          statementPtr,
-          "call isReadOnly",
-          new StatementOperation<Boolean>() {
-            @Override
-            public Boolean call(final SQLiteStatement statement) throws Exception {
-              return statement.isReadOnly();
-            }
-          });
-  }
-
-  long executeForLong(final long connectionPtr, final long statementPtr) {
-      return executeStatementOperation(
-          connectionPtr,
-          statementPtr,
-          "execute for long",
-          new StatementOperation<Long>() {
-            @Override
-            public Long call(final SQLiteStatement statement) throws Exception {
-              if (!statement.step()) {
-                throw new SQLiteException(
-                    SQLiteConstants.SQLITE_DONE, "No rows returned from query");
-              }
-              return statement.columnLong(0);
-            }
-          });
-  }
-
-  void executeStatement(final long connectionPtr, final long statementPtr) {
-    if (statementPtr == IGNORED_REINDEX_STMT) {
-      return;
-    }
-
-      executeStatementOperation(
-          connectionPtr,
-          statementPtr,
-          "execute",
-          new StatementOperation<Void>() {
-            @Override
-            public Void call(final SQLiteStatement statement) throws Exception {
-              statement.stepThrough();
-              return null;
-            }
-          });
-  }
-
-  String executeForString(final long connectionPtr, final long statementPtr) {
-      return executeStatementOperation(
-          connectionPtr,
-          statementPtr,
-          "execute for string",
-          new StatementOperation<String>() {
-            @Override
-            public String call(final SQLiteStatement statement) throws Exception {
-              if (!statement.step()) {
-                throw new SQLiteException(
-                    SQLiteConstants.SQLITE_DONE, "No rows returned from query");
-              }
-              return statement.columnString(0);
-            }
-          });
-  }
-
-  int getColumnCount(final long connectionPtr, final long statementPtr) {
-      return executeStatementOperation(
-          connectionPtr,
-          statementPtr,
-          "get columns count",
-          new StatementOperation<Integer>() {
-            @Override
-            public Integer call(final SQLiteStatement statement) throws Exception {
-              return statement.columnCount();
-            }
-          });
-  }
-
-  String getColumnName(final long connectionPtr, final long statementPtr, final int index) {
-      return executeStatementOperation(
-          connectionPtr,
-          statementPtr,
-          "get column name at index " + index,
-          new StatementOperation<String>() {
-            @Override
-            public String call(final SQLiteStatement statement) throws Exception {
-              return statement.getColumnName(index);
-            }
-          });
-  }
-
-  void bindNull(final long connectionPtr, final long statementPtr, final int index) {
-      executeStatementOperation(
-          connectionPtr,
-          statementPtr,
-          "bind null at index " + index,
-          new StatementOperation<Void>() {
-            @Override
-            public Void call(final SQLiteStatement statement) throws Exception {
-              statement.bindNull(index);
-              return null;
-            }
-          });
-  }
-
-  void bindLong(final long connectionPtr, final long statementPtr, final int index, final long value) {
-      executeStatementOperation(
-          connectionPtr,
-          statementPtr,
-          "bind long at index " + index + " with value " + value,
-          new StatementOperation<Void>() {
-            @Override
-            public Void call(final SQLiteStatement statement) throws Exception {
-              statement.bind(index, value);
-              return null;
-            }
-          });
-  }
-
-  void bindDouble(final long connectionPtr, final long statementPtr, final int index, final double value) {
-      executeStatementOperation(
-          connectionPtr,
-          statementPtr,
-          "bind double at index " + index + " with value " + value,
-          new StatementOperation<Void>() {
-            @Override
-            public Void call(final SQLiteStatement statement) throws Exception {
-              statement.bind(index, value);
-              return null;
-            }
-          });
-  }
-
-  void bindString(final long connectionPtr, final long statementPtr, final int index, final String value) {
-      executeStatementOperation(
-          connectionPtr,
-          statementPtr,
-          "bind string at index " + index,
-          new StatementOperation<Void>() {
-            @Override
-            public Void call(final SQLiteStatement statement) throws Exception {
-              statement.bind(index, value);
-              return null;
-            }
-          });
-  }
-
-  void bindBlob(final long connectionPtr, final long statementPtr, final int index, final byte[] value) {
-      executeStatementOperation(
-          connectionPtr,
-          statementPtr,
-          "bind blob at index " + index,
-          new StatementOperation<Void>() {
-            @Override
-            public Void call(final SQLiteStatement statement) throws Exception {
-              statement.bind(index, value);
-              return null;
-            }
-          });
-  }
-
-  int executeForChangedRowCount(final long connectionPtr, final long statementPtr) {
-    synchronized (lock) {
-      final SQLiteConnection connection = getConnection(connectionPtr);
-      final SQLiteStatement statement = getStatement(connectionPtr, statementPtr);
-
-        return execute(
-            "execute for changed row count",
-            new Callable<Integer>() {
-              @Override
-              public Integer call() throws Exception {
-                if (statement.step()) {
-                  throw new android.database.sqlite.SQLiteException(
-                      "Queries can be performed using SQLiteDatabase query or rawQuery methods"
-                          + " only.");
-                }
-                return connection.getChanges();
-              }
-            });
-    }
-  }
-
-  long executeForLastInsertedRowId(final long connectionPtr, final long statementPtr) {
-    synchronized (lock) {
-      final SQLiteConnection connection = getConnection(connectionPtr);
-      final SQLiteStatement statement = getStatement(connectionPtr, statementPtr);
-
-        return execute(
-            "execute for last inserted row ID",
-            new Callable<Long>() {
-              @Override
-              public Long call() throws Exception {
-                statement.stepThrough();
-                return connection.getChanges() > 0 ? connection.getLastInsertId() : -1L;
-              }
-            });
-    }
-  }
-
-  long executeForCursorWindow(final long connectionPtr, final long statementPtr, final long windowPtr) {
-      return executeStatementOperation(
-          connectionPtr,
-          statementPtr,
-          "execute for cursor window",
-          new StatementOperation<Integer>() {
-            @Override
-            public Integer call(final SQLiteStatement statement) throws Exception {
-              return ShadowCursorWindow.setData(windowPtr, statement);
-            }
-          });
-  }
-
-  void resetStatementAndClearBindings(final long connectionPtr, final long statementPtr) {
-      executeStatementOperation(
-          connectionPtr,
-          statementPtr,
-          "reset statement",
-          new StatementOperation<Void>() {
-            @Override
-            public Void call(final SQLiteStatement statement) throws Exception {
-              statement.reset(true);
-              return null;
-            }
-          });
-  }
-
-  interface StatementOperation<T> {
-    T call(final SQLiteStatement statement) throws Exception;
-  }
-
-  private <T> T executeStatementOperation(final long connectionPtr,
-                                          final long statementPtr,
-                                          final String comment,
-                                          final StatementOperation<T> statementOperation) {
-    synchronized (lock) {
-      final SQLiteStatement statement = getStatement(connectionPtr, statementPtr);
-      return execute(comment, new Callable<T>() {
-        @Override
-        public T call() throws Exception {
-          return statementOperation.call(statement);
-        }
-      });
-    }
-  }
-
-  /**
-   * Any Callable passed in to execute must not synchronize on lock, as this will result in a deadlock
-   */
-  private <T> T execute(final String comment, final Callable<T> work) {
-    synchronized (lock) {
-        return PerfStatsCollector.getInstance()
-            .measure("sqlite", () -> getFuture(comment, dbExecutor.submit(work)));
-    }
-  }
-
-  private static <T> T getFuture(final String comment, final Future<T> future) {
-    try {
-      return Uninterruptibles.getUninterruptibly(future);
-      // No need to catch cancellationexception - we never cancel these futures
-    } catch (ExecutionException e) {
-      Throwable t = e.getCause();
-      if (t instanceof SQLiteException) {
-          final RuntimeException sqlException =
-              getSqliteException("Cannot " + comment, ((SQLiteException) t).getBaseErrorCode());
-        sqlException.initCause(e);
-        throw sqlException;
-        } else if (t instanceof android.database.sqlite.SQLiteException) {
-          throw (android.database.sqlite.SQLiteException) t;
-      } else {
-        throw new RuntimeException(e);
-      }
-    }
-  }
-
-  private static RuntimeException getSqliteException(final String message, final int baseErrorCode) {
-    // Mapping is from throw_sqlite3_exception in android_database_SQLiteCommon.cpp
-    switch (baseErrorCode) {
-      case SQLiteConstants.SQLITE_ABORT: return new SQLiteAbortException(message);
-      case SQLiteConstants.SQLITE_PERM: return new SQLiteAccessPermException(message);
-      case SQLiteConstants.SQLITE_RANGE: return new SQLiteBindOrColumnIndexOutOfRangeException(message);
-      case SQLiteConstants.SQLITE_TOOBIG: return new SQLiteBlobTooBigException(message);
-      case SQLiteConstants.SQLITE_CANTOPEN: return new SQLiteCantOpenDatabaseException(message);
-      case SQLiteConstants.SQLITE_CONSTRAINT: return new SQLiteConstraintException(message);
-      case SQLiteConstants.SQLITE_NOTADB: // fall through
-      case SQLiteConstants.SQLITE_CORRUPT: return new SQLiteDatabaseCorruptException(message);
-      case SQLiteConstants.SQLITE_BUSY: return new SQLiteDatabaseLockedException(message);
-      case SQLiteConstants.SQLITE_MISMATCH: return new SQLiteDatatypeMismatchException(message);
-      case SQLiteConstants.SQLITE_IOERR: return new SQLiteDiskIOException(message);
-      case SQLiteConstants.SQLITE_DONE: return new SQLiteDoneException(message);
-      case SQLiteConstants.SQLITE_FULL: return new SQLiteFullException(message);
-      case SQLiteConstants.SQLITE_MISUSE: return new SQLiteMisuseException(message);
-      case SQLiteConstants.SQLITE_NOMEM: return new SQLiteOutOfMemoryException(message);
-      case SQLiteConstants.SQLITE_READONLY: return new SQLiteReadOnlyDatabaseException(message);
-      case SQLiteConstants.SQLITE_LOCKED: return new SQLiteTableLockedException(message);
-      case SQLiteConstants.SQLITE_INTERRUPT: return new OperationCanceledException(message);
-      default: return new android.database.sqlite.SQLiteException(message
-          + ", base error code: " + baseErrorCode);
-    }
-  }
-}
 }