Snap for 9416697 from dec7fbfd56318f64da1b81163754f2cdd7f457e2 to tm-qpr2-release

Change-Id: Ic414be28b875aa6289472995c18e9390e12beb08
diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
index b6f8575..c6d887e 100644
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/tests.yml
@@ -10,6 +10,9 @@
 permissions:
   contents: read
 
+env:
+  cache-version: v1
+
 jobs:
   build:
     runs-on: ubuntu-20.04
@@ -99,7 +102,7 @@
           path: '**/build/test-results/**/TEST-*.xml'
 
   instrumentation-tests:
-    runs-on: macos-11
+    runs-on: macos-12
     timeout-minutes: 60
     needs: build
 
@@ -135,7 +138,7 @@
           path: |
             ~/.android/avd/*
             ~/.android/adb*
-          key: avd-${{ matrix.api-level }}
+          key: avd-${{ matrix.api-level }}-${{ env.cache-version }}
 
       - name: Create AVD and generate snapshot for caching
         if: steps.avd-cache.outputs.cache-hit != 'true'
@@ -156,6 +159,11 @@
           api-level: ${{ matrix.api-level }}
           target: ${{ steps.determine-target.outputs.TARGET }}
           arch: x86_64
+          force-avd-creation: false
+          emulator-options: -no-snapshot-save -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none
+          disable-animations: true
+          disable-spellchecker: true
+
           profile: Nexus One
           script: |
             ./gradlew cAT || ./gradlew cAT || ./gradlew cAT || exit 1
diff --git a/.gitignore b/.gitignore
index 03ef5e0..cae6b80 100644
--- a/.gitignore
+++ b/.gitignore
@@ -21,6 +21,9 @@
 .gradle/
 build
 
+# Android Profiling
+*.hprof
+
 # IntelliJ
 .idea
 *.iml
@@ -40,7 +43,6 @@
 tmp
 local.properties
 
-
 # CTS stuff
 cts/
 cts-libs/
diff --git a/annotations/src/main/java/org/robolectric/annotation/GraphicsMode.java b/annotations/src/main/java/org/robolectric/annotation/GraphicsMode.java
new file mode 100644
index 0000000..06f785a
--- /dev/null
+++ b/annotations/src/main/java/org/robolectric/annotation/GraphicsMode.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 graphics
+ * shadow implementation is used for the {@link android.graphics} package.
+ */
+@Documented
+@Retention(RetentionPolicy.RUNTIME)
+@Target({ElementType.PACKAGE, ElementType.TYPE, ElementType.METHOD})
+public @interface GraphicsMode {
+
+  /** Specifies the different supported graphics modes. */
+  enum Mode {
+    /** Use legacy graphics shadows that are no-ops and fakes. */
+    LEGACY,
+    /** Use graphics shadows libraries backed by native Android graphics code. */
+    NATIVE,
+  }
+
+  Mode value();
+}
diff --git a/buildSrc/src/main/groovy/org/robolectric/gradle/GradleManagedDevicePlugin.groovy b/buildSrc/src/main/groovy/org/robolectric/gradle/GradleManagedDevicePlugin.groovy
index 7289d0c..c170f30 100644
--- a/buildSrc/src/main/groovy/org/robolectric/gradle/GradleManagedDevicePlugin.groovy
+++ b/buildSrc/src/main/groovy/org/robolectric/gradle/GradleManagedDevicePlugin.groovy
@@ -8,6 +8,7 @@
     @Override
     void apply(Project project) {
         project.android.testOptions {
+            animationsDisabled = true
             devices {
                 // ./gradlew -Pandroid.sdk.channel=3 nexusOneApi29DebugAndroidTest
                 nexusOneApi29(ManagedVirtualDevice) {
diff --git a/errorprone/src/main/java/org/robolectric/errorprone/bugpatterns/RobolectricShadow.java b/errorprone/src/main/java/org/robolectric/errorprone/bugpatterns/RobolectricShadow.java
index b5aceb4..06c634f 100644
--- a/errorprone/src/main/java/org/robolectric/errorprone/bugpatterns/RobolectricShadow.java
+++ b/errorprone/src/main/java/org/robolectric/errorprone/bugpatterns/RobolectricShadow.java
@@ -20,10 +20,12 @@
 import com.sun.source.doctree.TextTree;
 import com.sun.source.tree.AnnotationTree;
 import com.sun.source.tree.ClassTree;
+import com.sun.source.tree.CompilationUnitTree;
 import com.sun.source.tree.ExpressionTree;
 import com.sun.source.tree.IdentifierTree;
 import com.sun.source.tree.MethodTree;
 import com.sun.source.tree.ModifiersTree;
+import com.sun.source.util.DocSourcePositions;
 import com.sun.source.util.DocTreePath;
 import com.sun.source.util.DocTreePathScanner;
 import com.sun.source.util.TreePathScanner;
@@ -31,7 +33,6 @@
 import com.sun.tools.javac.code.Symbol;
 import com.sun.tools.javac.tree.DCTree.DCDocComment;
 import com.sun.tools.javac.tree.DCTree.DCReference;
-import com.sun.tools.javac.tree.DCTree.DCStartElement;
 import com.sun.tools.javac.tree.JCTree.JCAssign;
 import com.sun.tools.javac.tree.JCTree.JCIdent;
 import java.util.ArrayList;
@@ -113,11 +114,12 @@
     @Override
     public Void visitStartElement(StartElementTree startElementTree, Void aVoid) {
       if (startElementTree.getName().toString().equalsIgnoreCase("p")) {
-        DCStartElement node = (DCStartElement) startElementTree;
-
         DocTreePath path = getCurrentPath();
-        int start = (int) node.getSourcePosition((DCDocComment) path.getDocComment()) + node.pos;
-        int end = node.getEndPos((DCDocComment) getCurrentPath().getDocComment());
+        DCDocComment doc = (DCDocComment) path.getDocComment();
+        DocSourcePositions positions = trees.getSourcePositions();
+        CompilationUnitTree compilationUnitTree = path.getTreePath().getCompilationUnit();
+        int start = (int) positions.getStartPosition(compilationUnitTree, doc, startElementTree);
+        int end = (int) positions.getEndPosition(compilationUnitTree, doc, startElementTree);
 
         fixes.add(Optional.of(SuggestedFix.replace(start, end, "")));
       }
diff --git a/integration_tests/ctesque/src/sharedTest/java/android/graphics/BitmapTest.java b/integration_tests/ctesque/src/sharedTest/java/android/graphics/BitmapTest.java
index fddac6f..319b873 100644
--- a/integration_tests/ctesque/src/sharedTest/java/android/graphics/BitmapTest.java
+++ b/integration_tests/ctesque/src/sharedTest/java/android/graphics/BitmapTest.java
@@ -11,6 +11,7 @@
 import static androidx.test.InstrumentationRegistry.getTargetContext;
 import static com.google.common.truth.Truth.assertThat;
 import static org.junit.Assert.assertThrows;
+import static org.junit.Assume.assumeFalse;
 
 import android.content.res.Resources;
 import android.graphics.Bitmap.CompressFormat;
@@ -50,6 +51,7 @@
   @Config(minSdk = P)
   @SdkSuppress(minSdkVersion = P)
   @Test public void createBitmap() {
+    assumeFalse(Boolean.getBoolean("robolectric.nativeruntime.enableGraphics"));
     // Bitmap.createBitmap(Picture) requires hardware-backed bitmaps
     HardwareRendererCompat.setDrawingEnabled(true);
     Picture picture = new Picture();
diff --git a/integration_tests/ctesque/src/sharedTest/java/android/text/format/DateFormatTest.java b/integration_tests/ctesque/src/sharedTest/java/android/text/format/DateFormatTest.java
index 651e2c4..334356b 100644
--- a/integration_tests/ctesque/src/sharedTest/java/android/text/format/DateFormatTest.java
+++ b/integration_tests/ctesque/src/sharedTest/java/android/text/format/DateFormatTest.java
@@ -60,13 +60,15 @@
 
   @Test
   public void getTimeFormat_am() {
+    // allow both regular and thin whitespace separators
     assertThat(DateFormat.getTimeFormat(getApplicationContext()).format(dateAM))
-        .isEqualTo("8:24 AM");
+        .matches("8:24\\sAM");
   }
 
   @Test
   public void getTimeFormat_pm() {
+    // allow both regular and thin whitespace separators
     assertThat(DateFormat.getTimeFormat(getApplicationContext()).format(datePM))
-        .isEqualTo("4:24 PM");
+        .matches("4:24\\sPM");
   }
 }
diff --git a/nativeruntime/build.gradle b/nativeruntime/build.gradle
index ecfe569..495bf65 100644
--- a/nativeruntime/build.gradle
+++ b/nativeruntime/build.gradle
@@ -183,13 +183,16 @@
 
 dependencies {
   api project(":utils")
-
-  annotationProcessor "com.google.auto.service:auto-service:$autoServiceVersion"
+  api project(":utils:reflector")
   api "com.google.guava:guava:$guavaJREVersion"
 
+  annotationProcessor "com.google.auto.service:auto-service:$autoServiceVersion"
   compileOnly "com.google.auto.service:auto-service-annotations:$autoServiceVersion"
   compileOnly AndroidSdk.MAX_SDK.coordinates
 
+  testCompileOnly AndroidSdk.MAX_SDK.coordinates
+  testRuntimeOnly AndroidSdk.MAX_SDK.coordinates
+  testImplementation project(":robolectric")
   testImplementation "junit:junit:${junitVersion}"
   testImplementation "com.google.truth:truth:${truthVersion}"
 }
diff --git a/nativeruntime/src/main/java/org/robolectric/nativeruntime/AnimatedImageDrawableNatives.java b/nativeruntime/src/main/java/org/robolectric/nativeruntime/AnimatedImageDrawableNatives.java
new file mode 100644
index 0000000..a5bcfb5
--- /dev/null
+++ b/nativeruntime/src/main/java/org/robolectric/nativeruntime/AnimatedImageDrawableNatives.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.robolectric.nativeruntime;
+
+import android.graphics.ImageDecoder;
+import android.graphics.Rect;
+import android.graphics.drawable.AnimatedImageDrawable;
+
+/**
+ * Native methods for AnimatedImageDrawable JNI registration.
+ *
+ * <p>Native method signatures are derived from
+ * https://cs.android.com/android/platform/superproject/+/android-12.0.0_r1:frameworks/base/graphics/java/android/graphics/drawable/AnimatedImageDrawable.java
+ */
+public final class AnimatedImageDrawableNatives {
+  public static native long nCreate(
+      long nativeImageDecoder,
+      ImageDecoder decoder,
+      int width,
+      int height,
+      long colorSpaceHandle,
+      boolean extended,
+      Rect cropRect);
+
+  public static native long nGetNativeFinalizer();
+
+  public static native long nDraw(long nativePtr, long canvasNativePtr);
+
+  public static native void nSetAlpha(long nativePtr, int alpha);
+
+  public static native int nGetAlpha(long nativePtr);
+
+  public static native void nSetColorFilter(long nativePtr, long nativeFilter);
+
+  public static native boolean nIsRunning(long nativePtr);
+
+  public static native boolean nStart(long nativePtr);
+
+  public static native boolean nStop(long nativePtr);
+
+  public static native int nGetRepeatCount(long nativePtr);
+
+  public static native void nSetRepeatCount(long nativePtr, int repeatCount);
+
+  public static native void nSetOnAnimationEndListener(
+      long nativePtr, AnimatedImageDrawable drawable);
+
+  public static native long nNativeByteSize(long nativePtr);
+
+  public static native void nSetMirrored(long nativePtr, boolean mirror);
+
+  public static native void nSetBounds(long nativePtr, Rect rect);
+
+  private AnimatedImageDrawableNatives() {}
+}
diff --git a/nativeruntime/src/main/java/org/robolectric/nativeruntime/AnimatedVectorDrawableNatives.java b/nativeruntime/src/main/java/org/robolectric/nativeruntime/AnimatedVectorDrawableNatives.java
new file mode 100644
index 0000000..6d71e6c
--- /dev/null
+++ b/nativeruntime/src/main/java/org/robolectric/nativeruntime/AnimatedVectorDrawableNatives.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.robolectric.nativeruntime;
+
+import android.graphics.drawable.AnimatedVectorDrawable.VectorDrawableAnimatorRT;
+
+/**
+ * Native methods for AnimatedVectorDrawable JNI registration.
+ *
+ * <p>Native method signatures are derived from
+ * https://cs.android.com/android/platform/superproject/+/android-12.0.0_r1:frameworks/base/graphics/java/android/graphics/drawable/AnimatedVectorDrawable.java
+ */
+public final class AnimatedVectorDrawableNatives {
+
+  public static native long nCreateAnimatorSet();
+
+  public static native void nSetVectorDrawableTarget(long animatorPtr, long vectorDrawablePtr);
+
+  public static native void nAddAnimator(
+      long setPtr,
+      long propertyValuesHolder,
+      long nativeInterpolator,
+      long startDelay,
+      long duration,
+      int repeatCount,
+      int repeatMode);
+
+  public static native void nSetPropertyHolderData(long nativePtr, float[] data, int length);
+
+  public static native void nSetPropertyHolderData(long nativePtr, int[] data, int length);
+
+  public static native void nStart(long animatorSetPtr, VectorDrawableAnimatorRT set, int id);
+
+  public static native void nReverse(long animatorSetPtr, VectorDrawableAnimatorRT set, int id);
+
+  public static native long nCreateGroupPropertyHolder(
+      long nativePtr, int propertyId, float startValue, float endValue);
+
+  public static native long nCreatePathDataPropertyHolder(
+      long nativePtr, long startValuePtr, long endValuePtr);
+
+  public static native long nCreatePathColorPropertyHolder(
+      long nativePtr, int propertyId, int startValue, int endValue);
+
+  public static native long nCreatePathPropertyHolder(
+      long nativePtr, int propertyId, float startValue, float endValue);
+
+  public static native long nCreateRootAlphaPropertyHolder(
+      long nativePtr, float startValue, float endValue);
+
+  public static native void nEnd(long animatorSetPtr);
+
+  public static native void nReset(long animatorSetPtr);
+
+  private AnimatedVectorDrawableNatives() {}
+}
diff --git a/nativeruntime/src/main/java/org/robolectric/nativeruntime/BaseCanvasNatives.java b/nativeruntime/src/main/java/org/robolectric/nativeruntime/BaseCanvasNatives.java
new file mode 100644
index 0000000..f8931ea
--- /dev/null
+++ b/nativeruntime/src/main/java/org/robolectric/nativeruntime/BaseCanvasNatives.java
@@ -0,0 +1,344 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.robolectric.nativeruntime;
+
+import android.annotation.ColorLong;
+
+/**
+ * Native methods for BaseCanvas JNI registration.
+ *
+ * <p>Native method signatures are derived from
+ * https://cs.android.com/android/platform/superproject/+/android-12.0.0_r1:frameworks/base/graphics/java/android/graphics/BaseCanvas.java
+ */
+public final class BaseCanvasNatives {
+
+  public static native void nDrawBitmap(
+      long nativeCanvas,
+      long bitmapHandle,
+      float left,
+      float top,
+      long nativePaintOrZero,
+      int canvasDensity,
+      int screenDensity,
+      int bitmapDensity);
+
+  public static native void nDrawBitmap(
+      long nativeCanvas,
+      long bitmapHandle,
+      float srcLeft,
+      float srcTop,
+      float srcRight,
+      float srcBottom,
+      float dstLeft,
+      float dstTop,
+      float dstRight,
+      float dstBottom,
+      long nativePaintOrZero,
+      int screenDensity,
+      int bitmapDensity);
+
+  public static native void nDrawBitmap(
+      long nativeCanvas,
+      int[] colors,
+      int offset,
+      int stride,
+      float x,
+      float y,
+      int width,
+      int height,
+      boolean hasAlpha,
+      long nativePaintOrZero);
+
+  public static native void nDrawColor(long nativeCanvas, int color, int mode);
+
+  public static native void nDrawColor(
+      long nativeCanvas, long nativeColorSpace, @ColorLong long color, int mode);
+
+  public static native void nDrawPaint(long nativeCanvas, long nativePaint);
+
+  public static native void nDrawPoint(long canvasHandle, float x, float y, long paintHandle);
+
+  public static native void nDrawPoints(
+      long canvasHandle, float[] pts, int offset, int count, long paintHandle);
+
+  public static native void nDrawLine(
+      long nativeCanvas, float startX, float startY, float stopX, float stopY, long nativePaint);
+
+  public static native void nDrawLines(
+      long canvasHandle, float[] pts, int offset, int count, long paintHandle);
+
+  public static native void nDrawRect(
+      long nativeCanvas, float left, float top, float right, float bottom, long nativePaint);
+
+  public static native void nDrawOval(
+      long nativeCanvas, float left, float top, float right, float bottom, long nativePaint);
+
+  public static native void nDrawCircle(
+      long nativeCanvas, float cx, float cy, float radius, long nativePaint);
+
+  public static native void nDrawArc(
+      long nativeCanvas,
+      float left,
+      float top,
+      float right,
+      float bottom,
+      float startAngle,
+      float sweep,
+      boolean useCenter,
+      long nativePaint);
+
+  public static native void nDrawRoundRect(
+      long nativeCanvas,
+      float left,
+      float top,
+      float right,
+      float bottom,
+      float rx,
+      float ry,
+      long nativePaint);
+
+  public static native void nDrawDoubleRoundRect(
+      long nativeCanvas,
+      float outerLeft,
+      float outerTop,
+      float outerRight,
+      float outerBottom,
+      float outerRx,
+      float outerRy,
+      float innerLeft,
+      float innerTop,
+      float innerRight,
+      float innerBottom,
+      float innerRx,
+      float innerRy,
+      long nativePaint);
+
+  public static native void nDrawDoubleRoundRect(
+      long nativeCanvas,
+      float outerLeft,
+      float outerTop,
+      float outerRight,
+      float outerBottom,
+      float[] outerRadii,
+      float innerLeft,
+      float innerTop,
+      float innerRight,
+      float innerBottom,
+      float[] innerRadii,
+      long nativePaint);
+
+  public static native void nDrawPath(long nativeCanvas, long nativePath, long nativePaint);
+
+  public static native void nDrawRegion(long nativeCanvas, long nativeRegion, long nativePaint);
+
+  public static native void nDrawNinePatch(
+      long nativeCanvas,
+      long nativeBitmap,
+      long ninePatch,
+      float dstLeft,
+      float dstTop,
+      float dstRight,
+      float dstBottom,
+      long nativePaintOrZero,
+      int screenDensity,
+      int bitmapDensity);
+
+  public static native void nDrawBitmapMatrix(
+      long nativeCanvas, long bitmapHandle, long nativeMatrix, long nativePaint);
+
+  public static native void nDrawBitmapMesh(
+      long nativeCanvas,
+      long bitmapHandle,
+      int meshWidth,
+      int meshHeight,
+      float[] verts,
+      int vertOffset,
+      int[] colors,
+      int colorOffset,
+      long nativePaint);
+
+  public static native void nDrawVertices(
+      long nativeCanvas,
+      int mode,
+      int n,
+      float[] verts,
+      int vertOffset,
+      float[] texs,
+      int texOffset,
+      int[] colors,
+      int colorOffset,
+      short[] indices,
+      int indexOffset,
+      int indexCount,
+      long nativePaint);
+
+  public static native void nDrawGlyphs(
+      long nativeCanvas,
+      int[] glyphIds,
+      float[] positions,
+      int glyphIdStart,
+      int positionStart,
+      int glyphCount,
+      long nativeFont,
+      long nativePaint);
+
+  public static native void nDrawText(
+      long nativeCanvas,
+      char[] text,
+      int index,
+      int count,
+      float x,
+      float y,
+      int flags,
+      long nativePaint);
+
+  public static native void nDrawText(
+      long nativeCanvas,
+      String text,
+      int start,
+      int end,
+      float x,
+      float y,
+      int flags,
+      long nativePaint);
+
+  // Variant for O..O_MR1 that includes a Typeface pointer.
+  public static native void nDrawText(
+      long nativeCanvas,
+      char[] text,
+      int index,
+      int count,
+      float x,
+      float y,
+      int flags,
+      long nativePaint,
+      long nativeTypeface);
+
+  // Variant for O..O_MR1 that includes a Typeface pointer.
+  public static native void nDrawText(
+      long nativeCanvas,
+      String text,
+      int start,
+      int end,
+      float x,
+      float y,
+      int flags,
+      long nativePaint,
+      long nativeTypeface);
+
+  public static native void nDrawTextRun(
+      long nativeCanvas,
+      String text,
+      int start,
+      int end,
+      int contextStart,
+      int contextEnd,
+      float x,
+      float y,
+      boolean isRtl,
+      long nativePaint);
+
+  public static native void nDrawTextRun(
+      long nativeCanvas,
+      char[] text,
+      int start,
+      int count,
+      int contextStart,
+      int contextCount,
+      float x,
+      float y,
+      boolean isRtl,
+      long nativePaint,
+      long nativPrecomputedText);
+
+  // Variant for O..O_MR1 that includes a Typeface pointer.
+  public static native void nDrawTextRun(
+      long nativeCanvas,
+      String text,
+      int start,
+      int end,
+      int contextStart,
+      int contextEnd,
+      float x,
+      float y,
+      boolean isRtl,
+      long nativePaint,
+      long nativeTypeface);
+
+  // Variant for O..O_MR1 that includes a Typeface pointer.
+  public static native void nDrawTextRunTypeface(
+      long nativeCanvas,
+      char[] text,
+      int start,
+      int count,
+      int contextStart,
+      int contextCount,
+      float x,
+      float y,
+      boolean isRtl,
+      long nativePaint,
+      long nativeTypeface);
+
+  public static native void nDrawTextOnPath(
+      long nativeCanvas,
+      char[] text,
+      int index,
+      int count,
+      long nativePath,
+      float hOffset,
+      float vOffset,
+      int bidiFlags,
+      long nativePaint);
+
+  public static native void nDrawTextOnPath(
+      long nativeCanvas,
+      String text,
+      long nativePath,
+      float hOffset,
+      float vOffset,
+      int flags,
+      long nativePaint);
+
+  // Variant for O..O_MR1 that includes a Typeface pointer.
+  public static native void nDrawTextOnPath(
+      long nativeCanvas,
+      char[] text,
+      int index,
+      int count,
+      long nativePath,
+      float hOffset,
+      float vOffset,
+      int bidiFlags,
+      long nativePaint,
+      long nativeTypeface);
+
+  // Variant for O..O_MR1 that includes a Typeface pointer.
+  public static native void nDrawTextOnPath(
+      long nativeCanvas,
+      String text,
+      long nativePath,
+      float hOffset,
+      float vOffset,
+      int flags,
+      long nativePaint,
+      long nativeTypeface);
+
+  public static native void nPunchHole(
+      long renderer, float left, float top, float right, float bottom, float rx, float ry);
+
+  private BaseCanvasNatives() {}
+}
diff --git a/nativeruntime/src/main/java/org/robolectric/nativeruntime/BaseRecordingCanvasNatives.java b/nativeruntime/src/main/java/org/robolectric/nativeruntime/BaseRecordingCanvasNatives.java
new file mode 100644
index 0000000..3bd3fa9
--- /dev/null
+++ b/nativeruntime/src/main/java/org/robolectric/nativeruntime/BaseRecordingCanvasNatives.java
@@ -0,0 +1,343 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.robolectric.nativeruntime;
+
+import android.annotation.ColorLong;
+
+/**
+ * Native methods for BaseRecordingCanvas JNI registration.
+ *
+ * <p>Native method signatures are derived from
+ * https://cs.android.com/android/platform/superproject/+/android-12.0.0_r1:frameworks/base/graphics/java/android/graphics/BaseRecordingCanvas.java
+ */
+public final class BaseRecordingCanvasNatives {
+  public static native void nDrawBitmap(
+      long nativeCanvas,
+      long bitmapHandle,
+      float left,
+      float top,
+      long nativePaintOrZero,
+      int canvasDensity,
+      int screenDensity,
+      int bitmapDensity);
+
+  public static native void nDrawBitmap(
+      long nativeCanvas,
+      long bitmapHandle,
+      float srcLeft,
+      float srcTop,
+      float srcRight,
+      float srcBottom,
+      float dstLeft,
+      float dstTop,
+      float dstRight,
+      float dstBottom,
+      long nativePaintOrZero,
+      int screenDensity,
+      int bitmapDensity);
+
+  public static native void nDrawBitmap(
+      long nativeCanvas,
+      int[] colors,
+      int offset,
+      int stride,
+      float x,
+      float y,
+      int width,
+      int height,
+      boolean hasAlpha,
+      long nativePaintOrZero);
+
+  public static native void nDrawColor(long nativeCanvas, int color, int mode);
+
+  public static native void nDrawColor(
+      long nativeCanvas, long nativeColorSpace, @ColorLong long color, int mode);
+
+  public static native void nDrawPaint(long nativeCanvas, long nativePaint);
+
+  public static native void nDrawPoint(long canvasHandle, float x, float y, long paintHandle);
+
+  public static native void nDrawPoints(
+      long canvasHandle, float[] pts, int offset, int count, long paintHandle);
+
+  public static native void nDrawLine(
+      long nativeCanvas, float startX, float startY, float stopX, float stopY, long nativePaint);
+
+  public static native void nDrawLines(
+      long canvasHandle, float[] pts, int offset, int count, long paintHandle);
+
+  public static native void nDrawRect(
+      long nativeCanvas, float left, float top, float right, float bottom, long nativePaint);
+
+  public static native void nDrawOval(
+      long nativeCanvas, float left, float top, float right, float bottom, long nativePaint);
+
+  public static native void nDrawCircle(
+      long nativeCanvas, float cx, float cy, float radius, long nativePaint);
+
+  public static native void nDrawArc(
+      long nativeCanvas,
+      float left,
+      float top,
+      float right,
+      float bottom,
+      float startAngle,
+      float sweep,
+      boolean useCenter,
+      long nativePaint);
+
+  public static native void nDrawRoundRect(
+      long nativeCanvas,
+      float left,
+      float top,
+      float right,
+      float bottom,
+      float rx,
+      float ry,
+      long nativePaint);
+
+  public static native void nDrawDoubleRoundRect(
+      long nativeCanvas,
+      float outerLeft,
+      float outerTop,
+      float outerRight,
+      float outerBottom,
+      float outerRx,
+      float outerRy,
+      float innerLeft,
+      float innerTop,
+      float innerRight,
+      float innerBottom,
+      float innerRx,
+      float innerRy,
+      long nativePaint);
+
+  public static native void nDrawDoubleRoundRect(
+      long nativeCanvas,
+      float outerLeft,
+      float outerTop,
+      float outerRight,
+      float outerBottom,
+      float[] outerRadii,
+      float innerLeft,
+      float innerTop,
+      float innerRight,
+      float innerBottom,
+      float[] innerRadii,
+      long nativePaint);
+
+  public static native void nDrawPath(long nativeCanvas, long nativePath, long nativePaint);
+
+  public static native void nDrawRegion(long nativeCanvas, long nativeRegion, long nativePaint);
+
+  public static native void nDrawNinePatch(
+      long nativeCanvas,
+      long nativeBitmap,
+      long ninePatch,
+      float dstLeft,
+      float dstTop,
+      float dstRight,
+      float dstBottom,
+      long nativePaintOrZero,
+      int screenDensity,
+      int bitmapDensity);
+
+  public static native void nDrawBitmapMatrix(
+      long nativeCanvas, long bitmapHandle, long nativeMatrix, long nativePaint);
+
+  public static native void nDrawBitmapMesh(
+      long nativeCanvas,
+      long bitmapHandle,
+      int meshWidth,
+      int meshHeight,
+      float[] verts,
+      int vertOffset,
+      int[] colors,
+      int colorOffset,
+      long nativePaint);
+
+  public static native void nDrawVertices(
+      long nativeCanvas,
+      int mode,
+      int n,
+      float[] verts,
+      int vertOffset,
+      float[] texs,
+      int texOffset,
+      int[] colors,
+      int colorOffset,
+      short[] indices,
+      int indexOffset,
+      int indexCount,
+      long nativePaint);
+
+  public static native void nDrawGlyphs(
+      long nativeCanvas,
+      int[] glyphIds,
+      float[] positions,
+      int glyphIdStart,
+      int positionStart,
+      int glyphCount,
+      long nativeFont,
+      long nativePaint);
+
+  public static native void nDrawText(
+      long nativeCanvas,
+      char[] text,
+      int index,
+      int count,
+      float x,
+      float y,
+      int flags,
+      long nativePaint);
+
+  public static native void nDrawText(
+      long nativeCanvas,
+      String text,
+      int start,
+      int end,
+      float x,
+      float y,
+      int flags,
+      long nativePaint);
+
+  // Variant for O..O_MR1 that includes a Typeface pointer.
+  public static native void nDrawText(
+      long nativeCanvas,
+      char[] text,
+      int index,
+      int count,
+      float x,
+      float y,
+      int flags,
+      long nativePaint,
+      long nativeTypeface);
+
+  // Variant for O..O_MR1 that includes a Typeface pointer.
+  public static native void nDrawText(
+      long nativeCanvas,
+      String text,
+      int start,
+      int end,
+      float x,
+      float y,
+      int flags,
+      long nativePaint,
+      long nativeTypeface);
+
+  public static native void nDrawTextRun(
+      long nativeCanvas,
+      String text,
+      int start,
+      int end,
+      int contextStart,
+      int contextEnd,
+      float x,
+      float y,
+      boolean isRtl,
+      long nativePaint);
+
+  public static native void nDrawTextRun(
+      long nativeCanvas,
+      char[] text,
+      int start,
+      int count,
+      int contextStart,
+      int contextCount,
+      float x,
+      float y,
+      boolean isRtl,
+      long nativePaint,
+      long nativePrecomputedText);
+
+  // Variant for O..O_MR1 that includes a Typeface pointer.
+  public static native void nDrawTextRun(
+      long nativeCanvas,
+      String text,
+      int start,
+      int end,
+      int contextStart,
+      int contextEnd,
+      float x,
+      float y,
+      boolean isRtl,
+      long nativePaint,
+      long nativeTypeface);
+
+  // Variant for O..O_MR1 that includes a Typeface pointer.
+  public static native void nDrawTextRunTypeface(
+      long nativeCanvas,
+      char[] text,
+      int start,
+      int count,
+      int contextStart,
+      int contextCount,
+      float x,
+      float y,
+      boolean isRtl,
+      long nativePaint,
+      long nativeTypeface);
+
+  public static native void nDrawTextOnPath(
+      long nativeCanvas,
+      char[] text,
+      int index,
+      int count,
+      long nativePath,
+      float hOffset,
+      float vOffset,
+      int bidiFlags,
+      long nativePaint);
+
+  public static native void nDrawTextOnPath(
+      long nativeCanvas,
+      String text,
+      long nativePath,
+      float hOffset,
+      float vOffset,
+      int flags,
+      long nativePaint);
+
+  // Variant for O..O_MR1 that includes a Typeface pointer.
+  public static native void nDrawTextOnPath(
+      long nativeCanvas,
+      char[] text,
+      int index,
+      int count,
+      long nativePath,
+      float hOffset,
+      float vOffset,
+      int bidiFlags,
+      long nativePaint,
+      long nativeTypeface);
+
+  // Variant for O..O_MR1 that includes a Typeface pointer.
+  public static native void nDrawTextOnPath(
+      long nativeCanvas,
+      String text,
+      long nativePath,
+      float hOffset,
+      float vOffset,
+      int flags,
+      long nativePaint,
+      long nativeTypeface);
+
+  public static native void nPunchHole(
+      long renderer, float left, float top, float right, float bottom, float rx, float ry);
+
+  private BaseRecordingCanvasNatives() {}
+}
diff --git a/nativeruntime/src/main/java/org/robolectric/nativeruntime/BitmapFactoryNatives.java b/nativeruntime/src/main/java/org/robolectric/nativeruntime/BitmapFactoryNatives.java
new file mode 100644
index 0000000..8024f26
--- /dev/null
+++ b/nativeruntime/src/main/java/org/robolectric/nativeruntime/BitmapFactoryNatives.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.robolectric.nativeruntime;
+
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory.Options;
+import android.graphics.Rect;
+import java.io.FileDescriptor;
+import java.io.InputStream;
+
+/**
+ * Native methods for BitmapFactory JNI registration.
+ *
+ * <p>Native method signatures are derived from
+ * https://cs.android.com/android/platform/superproject/+/android-12.0.0_r1:frameworks/base/graphics/java/android/graphics/BitmapFactory.java
+ */
+public final class BitmapFactoryNatives {
+  public static native Bitmap nativeDecodeStream(
+      InputStream is,
+      byte[] storage,
+      Rect padding,
+      Options opts,
+      long inBitmapHandle,
+      long colorSpaceHandle);
+
+  public static native Bitmap nativeDecodeFileDescriptor(
+      FileDescriptor fd, Rect padding, Options opts, long inBitmapHandle, long colorSpaceHandle);
+
+  public static native Bitmap nativeDecodeAsset(
+      long nativeAsset, Rect padding, Options opts, long inBitmapHandle, long colorSpaceHandle);
+
+  public static native Bitmap nativeDecodeByteArray(
+      byte[] data,
+      int offset,
+      int length,
+      Options opts,
+      long inBitmapHandle,
+      long colorSpaceHandle);
+
+  public static native boolean nativeIsSeekable(FileDescriptor fd);
+
+  private BitmapFactoryNatives() {}
+}
diff --git a/nativeruntime/src/main/java/org/robolectric/nativeruntime/BitmapNatives.java b/nativeruntime/src/main/java/org/robolectric/nativeruntime/BitmapNatives.java
new file mode 100644
index 0000000..b273771
--- /dev/null
+++ b/nativeruntime/src/main/java/org/robolectric/nativeruntime/BitmapNatives.java
@@ -0,0 +1,134 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.robolectric.nativeruntime;
+
+import android.graphics.Bitmap;
+import android.graphics.ColorSpace;
+import android.hardware.HardwareBuffer;
+import android.os.Parcel;
+import java.io.OutputStream;
+import java.nio.Buffer;
+
+/**
+ * Native methods for Bitmap JNI registration.
+ *
+ * <p>Native method signatures are derived from
+ * https://cs.android.com/android/platform/superproject/+/android-12.0.0_r1:frameworks/base/graphics/java/android/graphics/Bitmap.java
+ */
+public final class BitmapNatives {
+
+  public static native Bitmap nativeCreate(
+      int[] colors,
+      int offset,
+      int stride,
+      int width,
+      int height,
+      int nativeConfig,
+      boolean mutable,
+      long nativeColorSpace);
+
+  public static native Bitmap nativeCopy(long nativeSrcBitmap, int nativeConfig, boolean isMutable);
+
+  public static native Bitmap nativeCopyAshmem(long nativeSrcBitmap);
+
+  public static native Bitmap nativeCopyAshmemConfig(long nativeSrcBitmap, int nativeConfig);
+
+  public static native long nativeGetNativeFinalizer();
+
+  public static native void nativeRecycle(long nativeBitmap);
+
+  public static native void nativeReconfigure(
+      long nativeBitmap, int width, int height, int config, boolean isPremultiplied);
+
+  public static native boolean nativeCompress(
+      long nativeBitmap, int format, int quality, OutputStream stream, byte[] tempStorage);
+
+  public static native void nativeErase(long nativeBitmap, int color);
+
+  public static native void nativeErase(long nativeBitmap, long colorSpacePtr, long color);
+
+  public static native int nativeRowBytes(long nativeBitmap);
+
+  public static native int nativeConfig(long nativeBitmap);
+
+  public static native int nativeGetPixel(long nativeBitmap, int x, int y);
+
+  public static native long nativeGetColor(long nativeBitmap, int x, int y);
+
+  public static native void nativeGetPixels(
+      long nativeBitmap, int[] pixels, int offset, int stride, int x, int y, int width, int height);
+
+  public static native void nativeSetPixel(long nativeBitmap, int x, int y, int color);
+
+  public static native void nativeSetPixels(
+      long nativeBitmap, int[] colors, int offset, int stride, int x, int y, int width, int height);
+
+  public static native void nativeCopyPixelsToBuffer(long nativeBitmap, Buffer dst);
+
+  public static native void nativeCopyPixelsFromBuffer(long nativeBitmap, Buffer src);
+
+  public static native int nativeGenerationId(long nativeBitmap);
+
+  public static native Bitmap nativeCreateFromParcel(Parcel p);
+  // returns true on success
+  public static native boolean nativeWriteToParcel(long nativeBitmap, int density, Parcel p);
+  // returns a new bitmap built from the native bitmap's alpha, and the paint
+  public static native Bitmap nativeExtractAlpha(
+      long nativeBitmap, long nativePaint, int[] offsetXY);
+
+  public static native boolean nativeHasAlpha(long nativeBitmap);
+
+  public static native boolean nativeIsPremultiplied(long nativeBitmap);
+
+  public static native void nativeSetPremultiplied(long nativeBitmap, boolean isPremul);
+
+  public static native void nativeSetHasAlpha(
+      long nativeBitmap, boolean hasAlpha, boolean requestPremul);
+
+  public static native boolean nativeHasMipMap(long nativeBitmap);
+
+  public static native void nativeSetHasMipMap(long nativeBitmap, boolean hasMipMap);
+
+  public static native boolean nativeSameAs(long nativeBitmap0, long nativeBitmap1);
+
+  public static native void nativePrepareToDraw(long nativeBitmap);
+
+  public static native int nativeGetAllocationByteCount(long nativeBitmap);
+
+  public static native Bitmap nativeCopyPreserveInternalConfig(long nativeBitmap);
+
+  public static native Bitmap nativeWrapHardwareBufferBitmap(
+      HardwareBuffer buffer, long nativeColorSpace);
+
+  public static native HardwareBuffer nativeGetHardwareBuffer(long nativeBitmap);
+
+  public static native ColorSpace nativeComputeColorSpace(long nativePtr);
+
+  public static native void nativeSetColorSpace(long nativePtr, long nativeColorSpace);
+
+  public static native boolean nativeIsSRGB(long nativePtr);
+
+  public static native boolean nativeIsSRGBLinear(long nativePtr);
+
+  public static native void nativeSetImmutable(long nativePtr);
+
+  public static native boolean nativeIsImmutable(long nativePtr);
+
+  public static native boolean nativeIsBackedByAshmem(long nativePtr);
+
+  private BitmapNatives() {}
+}
diff --git a/nativeruntime/src/main/java/org/robolectric/nativeruntime/BitmapShaderNatives.java b/nativeruntime/src/main/java/org/robolectric/nativeruntime/BitmapShaderNatives.java
new file mode 100644
index 0000000..f0d1df1
--- /dev/null
+++ b/nativeruntime/src/main/java/org/robolectric/nativeruntime/BitmapShaderNatives.java
@@ -0,0 +1,19 @@
+package org.robolectric.nativeruntime;
+
+/**
+ * Native methods for BitmapShader JNI registration.
+ *
+ * <p>Native method signatures are derived from
+ * https://cs.android.com/android/platform/superproject/+/android-12.0.0_r1:frameworks/base/graphics/java/android/graphics/BitmapShader.java
+ */
+public final class BitmapShaderNatives {
+
+  public static native long nativeCreate(
+      long nativeMatrix,
+      long bitmapHandle,
+      int shaderTileModeX,
+      int shaderTileModeY,
+      boolean filter);
+
+  private BitmapShaderNatives() {}
+}
diff --git a/nativeruntime/src/main/java/org/robolectric/nativeruntime/BlendModeColorFilterNatives.java b/nativeruntime/src/main/java/org/robolectric/nativeruntime/BlendModeColorFilterNatives.java
new file mode 100644
index 0000000..aa93a50
--- /dev/null
+++ b/nativeruntime/src/main/java/org/robolectric/nativeruntime/BlendModeColorFilterNatives.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2006 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.robolectric.nativeruntime;
+
+/**
+ * Native methods for BlendModeColorFilter JNI registration.
+ *
+ * <p>Native method signatures are derived from
+ * https://cs.android.com/android/platform/superproject/+/android-12.0.0_r1:frameworks/base/graphics/java/android/graphics/BlendModeColorFilter.java
+ */
+public final class BlendModeColorFilterNatives {
+
+  public static native long native_CreateBlendModeFilter(int srcColor, int blendmode);
+
+  private BlendModeColorFilterNatives() {}
+}
diff --git a/nativeruntime/src/main/java/org/robolectric/nativeruntime/BlurMaskFilterNatives.java b/nativeruntime/src/main/java/org/robolectric/nativeruntime/BlurMaskFilterNatives.java
new file mode 100644
index 0000000..72cdebc
--- /dev/null
+++ b/nativeruntime/src/main/java/org/robolectric/nativeruntime/BlurMaskFilterNatives.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2006 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.robolectric.nativeruntime;
+
+/**
+ * Native methods for BlurMaskFilter JNI registration.
+ *
+ * <p>Native method signatures are derived from
+ * https://cs.android.com/android/platform/superproject/+/android-12.0.0_r1:frameworks/base/graphics/java/android/graphics/BlurMaskFilter.java
+ */
+public final class BlurMaskFilterNatives {
+
+  public static native long nativeConstructor(float radius, int style);
+
+  private BlurMaskFilterNatives() {}
+}
diff --git a/nativeruntime/src/main/java/org/robolectric/nativeruntime/CanvasNatives.java b/nativeruntime/src/main/java/org/robolectric/nativeruntime/CanvasNatives.java
new file mode 100644
index 0000000..398ce96
--- /dev/null
+++ b/nativeruntime/src/main/java/org/robolectric/nativeruntime/CanvasNatives.java
@@ -0,0 +1,94 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.robolectric.nativeruntime;
+
+import android.graphics.Rect;
+
+/**
+ * Native methods for Canvas JNI registration.
+ *
+ * <p>Native method signatures are derived from
+ * https://cs.android.com/android/platform/superproject/+/android-12.0.0_r1:frameworks/base/graphics/java/android/graphics/Canvas.java
+ */
+public final class CanvasNatives {
+  public static native void nFreeCaches();
+
+  public static native void nFreeTextLayoutCaches();
+
+  public static native long nGetNativeFinalizer();
+
+  public static native void nSetCompatibilityVersion(int apiLevel);
+
+  public static native long nInitRaster(long bitmapHandle);
+
+  public static native void nSetBitmap(long canvasHandle, long bitmapHandle);
+
+  public static native boolean nGetClipBounds(long nativeCanvas, Rect bounds);
+
+  public static native boolean nIsOpaque(long canvasHandle);
+
+  public static native int nGetWidth(long canvasHandle);
+
+  public static native int nGetHeight(long canvasHandle);
+
+  public static native int nSave(long canvasHandle, int saveFlags);
+
+  public static native int nSaveLayer(
+      long nativeCanvas, float l, float t, float r, float b, long nativePaint);
+
+  public static native int nSaveLayerAlpha(
+      long nativeCanvas, float l, float t, float r, float b, int alpha);
+
+  public static native int nSaveUnclippedLayer(long nativeCanvas, int l, int t, int r, int b);
+
+  public static native void nRestoreUnclippedLayer(
+      long nativeCanvas, int saveCount, long nativePaint);
+
+  public static native boolean nRestore(long canvasHandle);
+
+  public static native void nRestoreToCount(long canvasHandle, int saveCount);
+
+  public static native int nGetSaveCount(long canvasHandle);
+
+  public static native void nTranslate(long canvasHandle, float dx, float dy);
+
+  public static native void nScale(long canvasHandle, float sx, float sy);
+
+  public static native void nRotate(long canvasHandle, float degrees);
+
+  public static native void nSkew(long canvasHandle, float sx, float sy);
+
+  public static native void nConcat(long nativeCanvas, long nativeMatrix);
+
+  public static native void nSetMatrix(long nativeCanvas, long nativeMatrix);
+
+  public static native boolean nClipRect(
+      long nativeCanvas, float left, float top, float right, float bottom, int regionOp);
+
+  public static native boolean nClipPath(long nativeCanvas, long nativePath, int regionOp);
+
+  public static native void nSetDrawFilter(long nativeCanvas, long nativeFilter);
+
+  public static native void nGetMatrix(long nativeCanvas, long nativeMatrix);
+
+  public static native boolean nQuickReject(long nativeCanvas, long nativePath);
+
+  public static native boolean nQuickReject(
+      long nativeCanvas, float left, float top, float right, float bottom);
+
+  private CanvasNatives() {}
+}
diff --git a/nativeruntime/src/main/java/org/robolectric/nativeruntime/CanvasPropertyNatives.java b/nativeruntime/src/main/java/org/robolectric/nativeruntime/CanvasPropertyNatives.java
new file mode 100644
index 0000000..8e229f6
--- /dev/null
+++ b/nativeruntime/src/main/java/org/robolectric/nativeruntime/CanvasPropertyNatives.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.robolectric.nativeruntime;
+
+/**
+ * Native methods for CanvasProperty JNI registration.
+ *
+ * <p>Native method signatures are derived from
+ * https://cs.android.com/android/platform/superproject/+/android-12.0.0_r1:frameworks/base/graphics/java/android/graphics/CanvasProperty.java
+ */
+public final class CanvasPropertyNatives {
+
+  public static native long nCreateFloat(float initialValue);
+
+  public static native long nCreatePaint(long initialValuePaintPtr);
+
+  private CanvasPropertyNatives() {}
+}
diff --git a/nativeruntime/src/main/java/org/robolectric/nativeruntime/ColorFilterNatives.java b/nativeruntime/src/main/java/org/robolectric/nativeruntime/ColorFilterNatives.java
new file mode 100644
index 0000000..174fc8f
--- /dev/null
+++ b/nativeruntime/src/main/java/org/robolectric/nativeruntime/ColorFilterNatives.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2006 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.robolectric.nativeruntime;
+
+/**
+ * Native methods for ColorFilter JNI registration.
+ *
+ * <p>Native method signatures are derived from
+ * https://cs.android.com/android/platform/superproject/+/android-12.0.0_r1:frameworks/base/graphics/java/android/graphics/ColorFilter.java
+ */
+public final class ColorFilterNatives {
+
+  public static native long nativeGetFinalizer();
+
+  public static native void nSafeUnref(long nativeFinalizer);
+
+  private ColorFilterNatives() {}
+}
diff --git a/nativeruntime/src/main/java/org/robolectric/nativeruntime/ColorMatrixColorFilterNatives.java b/nativeruntime/src/main/java/org/robolectric/nativeruntime/ColorMatrixColorFilterNatives.java
new file mode 100644
index 0000000..7b0da18
--- /dev/null
+++ b/nativeruntime/src/main/java/org/robolectric/nativeruntime/ColorMatrixColorFilterNatives.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2006 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.robolectric.nativeruntime;
+
+/**
+ * Native methods for ColorMatrixColorFilter JNI registration.
+ *
+ * <p>Native method signatures are derived from
+ * https://cs.android.com/android/platform/superproject/+/android-12.0.0_r1:frameworks/base/graphics/java/android/graphics/ColorMatrixColorFilter.java
+ */
+public final class ColorMatrixColorFilterNatives {
+
+  public static native long nativeColorMatrixFilter(float[] array);
+
+  private ColorMatrixColorFilterNatives() {}
+}
diff --git a/nativeruntime/src/main/java/org/robolectric/nativeruntime/ColorNatives.java b/nativeruntime/src/main/java/org/robolectric/nativeruntime/ColorNatives.java
new file mode 100644
index 0000000..1b0f0dd
--- /dev/null
+++ b/nativeruntime/src/main/java/org/robolectric/nativeruntime/ColorNatives.java
@@ -0,0 +1,16 @@
+package org.robolectric.nativeruntime;
+
+/**
+ * Native methods for Color JNI registration.
+ *
+ * <p>Native method signatures are derived from
+ * https://cs.android.com/android/platform/superproject/+/android-12.0.0_r1:frameworks/base/graphics/java/android/graphics/Color.java
+ */
+public final class ColorNatives {
+
+  public static native void nativeRGBToHSV(int red, int greed, int blue, float[] hsv);
+
+  public static native int nativeHSVToColor(int alpha, float[] hsv);
+
+  private ColorNatives() {}
+}
diff --git a/nativeruntime/src/main/java/org/robolectric/nativeruntime/ColorSpaceRgbNatives.java b/nativeruntime/src/main/java/org/robolectric/nativeruntime/ColorSpaceRgbNatives.java
new file mode 100644
index 0000000..24f70a3
--- /dev/null
+++ b/nativeruntime/src/main/java/org/robolectric/nativeruntime/ColorSpaceRgbNatives.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.robolectric.nativeruntime;
+
+/**
+ * Native methods for BitmapFactory JNI registration.
+ *
+ * <p>Native method signatures are derived from
+ * https://cs.android.com/android/platform/superproject/+/android-12.0.0_r1:frameworks/base/graphics/java/android/graphics/ColorSpace.java
+ */
+public final class ColorSpaceRgbNatives {
+
+  public static native long nativeGetNativeFinalizer();
+
+  public static native long nativeCreate(
+      float a, float b, float c, float d, float e, float f, float g, float[] xyz);
+
+  private ColorSpaceRgbNatives() {}
+}
diff --git a/nativeruntime/src/main/java/org/robolectric/nativeruntime/ComposePathEffectNatives.java b/nativeruntime/src/main/java/org/robolectric/nativeruntime/ComposePathEffectNatives.java
new file mode 100644
index 0000000..ed6133b
--- /dev/null
+++ b/nativeruntime/src/main/java/org/robolectric/nativeruntime/ComposePathEffectNatives.java
@@ -0,0 +1,14 @@
+package org.robolectric.nativeruntime;
+
+/**
+ * Native methods for ComposePathEffect JNI registration.
+ *
+ * <p>Native method signatures are derived from
+ * https://cs.android.com/android/platform/superproject/+/android-12.0.0_r1:frameworks/base/graphics/java/android/graphics/ComposePathEffect.java
+ */
+public final class ComposePathEffectNatives {
+
+  public static native long nativeCreate(long nativeOuterpe, long nativeInnerpe);
+
+  private ComposePathEffectNatives() {}
+}
diff --git a/nativeruntime/src/main/java/org/robolectric/nativeruntime/ComposeShaderNatives.java b/nativeruntime/src/main/java/org/robolectric/nativeruntime/ComposeShaderNatives.java
new file mode 100644
index 0000000..9e0982d
--- /dev/null
+++ b/nativeruntime/src/main/java/org/robolectric/nativeruntime/ComposeShaderNatives.java
@@ -0,0 +1,14 @@
+package org.robolectric.nativeruntime;
+
+/**
+ * Native methods for ComposeShader JNI registration.
+ *
+ * <p>Native method signatures are derived from
+ * https://cs.android.com/android/platform/superproject/+/android-12.0.0_r1:frameworks/base/graphics/java/android/graphics/ComposeShader.java
+ */
+public class ComposeShaderNatives {
+  public static native long nativeCreate(
+      long nativeMatrix, long nativeShaderA, long nativeShaderB, int porterDuffMode);
+
+  private ComposeShaderNatives() {}
+}
diff --git a/nativeruntime/src/main/java/org/robolectric/nativeruntime/CornerPathEffectNatives.java b/nativeruntime/src/main/java/org/robolectric/nativeruntime/CornerPathEffectNatives.java
new file mode 100644
index 0000000..44c572e
--- /dev/null
+++ b/nativeruntime/src/main/java/org/robolectric/nativeruntime/CornerPathEffectNatives.java
@@ -0,0 +1,13 @@
+package org.robolectric.nativeruntime;
+
+/**
+ * Native methods for CornerPathEffect JNI registration.
+ *
+ * <p>Native method signatures are derived from
+ * https://cs.android.com/android/platform/superproject/+/android-12.0.0_r1:frameworks/base/graphics/java/android/graphics/CornerPathEffect.java
+ */
+public final class CornerPathEffectNatives {
+  public static native long nativeCreate(float radius);
+
+  private CornerPathEffectNatives() {}
+}
diff --git a/nativeruntime/src/main/java/org/robolectric/nativeruntime/DashPathEffectNatives.java b/nativeruntime/src/main/java/org/robolectric/nativeruntime/DashPathEffectNatives.java
new file mode 100644
index 0000000..430a74f
--- /dev/null
+++ b/nativeruntime/src/main/java/org/robolectric/nativeruntime/DashPathEffectNatives.java
@@ -0,0 +1,13 @@
+package org.robolectric.nativeruntime;
+
+/**
+ * Native methods for DashPathEffect JNI registration.
+ *
+ * <p>Native method signatures are derived from
+ * https://cs.android.com/android/platform/superproject/+/android-12.0.0_r1:frameworks/base/graphics/java/android/graphics/DashPathEffect.java
+ */
+public final class DashPathEffectNatives {
+  public static native long nativeCreate(float[] intervals, float phase);
+
+  private DashPathEffectNatives() {}
+}
diff --git a/nativeruntime/src/main/java/org/robolectric/nativeruntime/DiscretePathEffectNatives.java b/nativeruntime/src/main/java/org/robolectric/nativeruntime/DiscretePathEffectNatives.java
new file mode 100644
index 0000000..f1bfa2f
--- /dev/null
+++ b/nativeruntime/src/main/java/org/robolectric/nativeruntime/DiscretePathEffectNatives.java
@@ -0,0 +1,13 @@
+package org.robolectric.nativeruntime;
+
+/**
+ * Native methods for DiscretePathEffect JNI registration.
+ *
+ * <p>Native method signatures are derived from
+ * https://cs.android.com/android/platform/superproject/+/android-12.0.0_r1:frameworks/base/graphics/java/android/graphics/DiscretePathEffect.java
+ */
+public final class DiscretePathEffectNatives {
+  public static native long nativeCreate(float length, float deviation);
+
+  private DiscretePathEffectNatives() {}
+}
diff --git a/nativeruntime/src/main/java/org/robolectric/nativeruntime/EmbossMaskFilterNatives.java b/nativeruntime/src/main/java/org/robolectric/nativeruntime/EmbossMaskFilterNatives.java
new file mode 100644
index 0000000..27ef3da
--- /dev/null
+++ b/nativeruntime/src/main/java/org/robolectric/nativeruntime/EmbossMaskFilterNatives.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2006 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.robolectric.nativeruntime;
+
+/**
+ * Native methods for EmbossMaskFilter JNI registration.
+ *
+ * <p>Native method signatures are derived from
+ * https://cs.android.com/android/platform/superproject/+/android-12.0.0_r1:frameworks/base/graphics/java/android/graphics/EmbossMaskFilter.java
+ */
+public final class EmbossMaskFilterNatives {
+
+  public static native long nativeConstructor(
+      float[] direction, float ambient, float specular, float blurRadius);
+
+  private EmbossMaskFilterNatives() {}
+}
diff --git a/nativeruntime/src/main/java/org/robolectric/nativeruntime/FontBuilderNatives.java b/nativeruntime/src/main/java/org/robolectric/nativeruntime/FontBuilderNatives.java
new file mode 100644
index 0000000..0a02b45
--- /dev/null
+++ b/nativeruntime/src/main/java/org/robolectric/nativeruntime/FontBuilderNatives.java
@@ -0,0 +1,47 @@
+package org.robolectric.nativeruntime;
+
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import java.nio.ByteBuffer;
+
+/**
+ * Native methods for android.graphics.fonts.Font$Builder JNI registration.
+ *
+ * <p>Native method signatures are derived from
+ * https://cs.android.com/android/platform/superproject/+/android-12.0.0_r1:frameworks/base/graphics/java/android/graphics/fonts/Font.java
+ */
+public final class FontBuilderNatives {
+  public static native long nInitBuilder();
+
+  public static native void nAddAxis(long builderPtr, int tag, float value);
+
+  public static native long nBuild(
+      long builderPtr,
+      ByteBuffer buffer,
+      String filePath,
+      String localeList,
+      int weight,
+      boolean italic,
+      int ttcIndex);
+
+  public static native long nGetReleaseNativeFont();
+
+  public static native long nClone(
+      long fontPtr, long builderPtr, int weight, boolean italic, int ttcIndex);
+
+  private FontBuilderNatives() {}
+}
diff --git a/nativeruntime/src/main/java/org/robolectric/nativeruntime/FontFamilyBuilderNatives.java b/nativeruntime/src/main/java/org/robolectric/nativeruntime/FontFamilyBuilderNatives.java
new file mode 100644
index 0000000..4bc5e7f
--- /dev/null
+++ b/nativeruntime/src/main/java/org/robolectric/nativeruntime/FontFamilyBuilderNatives.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.robolectric.nativeruntime;
+
+/**
+ * Native methods for android.graphics.fonts.FontFamily$Builder JNI registration.
+ *
+ * <p>Native method signatures are derived from
+ * https://cs.android.com/android/platform/superproject/+/android-12.0.0_r1:frameworks/base/graphics/java/android/graphics/fonts/FontFamily.java
+ */
+public final class FontFamilyBuilderNatives {
+
+  public static native long nInitBuilder();
+
+  public static native void nAddFont(long builderPtr, long fontPtr);
+
+  public static native long nBuild(
+      long builderPtr, String langTags, int variant, boolean isCustomFallback);
+
+  public static native long nGetReleaseNativeFamily();
+
+  private FontFamilyBuilderNatives() {}
+}
diff --git a/nativeruntime/src/main/java/org/robolectric/nativeruntime/FontFamilyNatives.java b/nativeruntime/src/main/java/org/robolectric/nativeruntime/FontFamilyNatives.java
new file mode 100644
index 0000000..933baff
--- /dev/null
+++ b/nativeruntime/src/main/java/org/robolectric/nativeruntime/FontFamilyNatives.java
@@ -0,0 +1,51 @@
+package org.robolectric.nativeruntime;
+
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import java.nio.ByteBuffer;
+
+/**
+ * Native methods for the deprecated android.graphics.FontFamily JNI registration. Note this is
+ * different from {@link FontsFontFamilyNatives}.
+ *
+ * <p>Native method signatures are derived from
+ * https://cs.android.com/android/platform/superproject/+/android-12.0.0_r1:frameworks/base/graphics/java/android/graphics/FontFamily.java
+ */
+public final class FontFamilyNatives {
+
+  public static native long nInitBuilder(String langs, int variant);
+
+  public static native void nAllowUnsupportedFont(long builderPtr);
+
+  public static native long nCreateFamily(long mBuilderPtr);
+
+  public static native long nGetBuilderReleaseFunc();
+
+  public static native long nGetFamilyReleaseFunc();
+  // By passing -1 to weight argument, the weight value is resolved by OS/2 table in the font.
+  // By passing -1 to italic argument, the italic value is resolved by OS/2 table in the font.
+  public static native boolean nAddFont(
+      long builderPtr, ByteBuffer font, int ttcIndex, int weight, int isItalic);
+
+  public static native boolean nAddFontWeightStyle(
+      long builderPtr, ByteBuffer font, int ttcIndex, int weight, int isItalic);
+
+  // The added axis values are only valid for the next nAddFont* method call.
+  public static native void nAddAxisValue(long builderPtr, int tag, float value);
+
+  private FontFamilyNatives() {}
+}
diff --git a/nativeruntime/src/main/java/org/robolectric/nativeruntime/FontFileUtilNatives.java b/nativeruntime/src/main/java/org/robolectric/nativeruntime/FontFileUtilNatives.java
new file mode 100644
index 0000000..613ecb9
--- /dev/null
+++ b/nativeruntime/src/main/java/org/robolectric/nativeruntime/FontFileUtilNatives.java
@@ -0,0 +1,35 @@
+package org.robolectric.nativeruntime;
+
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import java.nio.ByteBuffer;
+
+/**
+ * Native methods for android.graphics.fonts.FontFileUtil JNI registration.
+ *
+ * <p>Native method signatures are derived from
+ * https://cs.android.com/android/platform/superproject/+/android-12.0.0_r1:frameworks/base/graphics/java/android/graphics/fonts/FontFileUtil.java
+ */
+public final class FontFileUtilNatives {
+  public static native long nGetFontRevision(ByteBuffer buffer, int index);
+
+  public static native String nGetFontPostScriptName(ByteBuffer buffer, int index);
+
+  public static native int nIsPostScriptType1Font(ByteBuffer buffer, int index);
+
+  private FontFileUtilNatives() {}
+}
diff --git a/nativeruntime/src/main/java/org/robolectric/nativeruntime/FontNatives.java b/nativeruntime/src/main/java/org/robolectric/nativeruntime/FontNatives.java
new file mode 100644
index 0000000..bb1994f
--- /dev/null
+++ b/nativeruntime/src/main/java/org/robolectric/nativeruntime/FontNatives.java
@@ -0,0 +1,61 @@
+package org.robolectric.nativeruntime;
+
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import android.graphics.Paint;
+import android.graphics.RectF;
+import java.nio.ByteBuffer;
+
+/**
+ * Native methods for android.graphics.fonts.Font JNI registration.
+ *
+ * <p>Native method signatures are derived from
+ * https://cs.android.com/android/platform/superproject/+/android-12.0.0_r1:frameworks/base/graphics/java/android/graphics/fonts/Font.java
+ */
+public final class FontNatives {
+  public static native long nGetMinikinFontPtr(long font);
+
+  public static native long nCloneFont(long font);
+
+  public static native ByteBuffer nNewByteBuffer(long font);
+
+  public static native long nGetBufferAddress(long font);
+
+  public static native int nGetSourceId(long font);
+
+  public static native long nGetReleaseNativeFont();
+
+  public static native float nGetGlyphBounds(long font, int glyphId, long paint, RectF rect);
+
+  public static native float nGetFontMetrics(long font, long paint, Paint.FontMetrics metrics);
+
+  public static native String nGetFontPath(long fontPtr);
+
+  public static native String nGetLocaleList(long familyPtr);
+
+  public static native int nGetPackedStyle(long fontPtr);
+
+  public static native int nGetIndex(long fontPtr);
+
+  public static native int nGetAxisCount(long fontPtr);
+
+  public static native long nGetAxisInfo(long fontPtr, int i);
+
+  public static native long[] nGetAvailableFontSet();
+
+  private FontNatives() {}
+}
diff --git a/nativeruntime/src/main/java/org/robolectric/nativeruntime/FontsFontFamilyNatives.java b/nativeruntime/src/main/java/org/robolectric/nativeruntime/FontsFontFamilyNatives.java
new file mode 100644
index 0000000..20379d5
--- /dev/null
+++ b/nativeruntime/src/main/java/org/robolectric/nativeruntime/FontsFontFamilyNatives.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.robolectric.nativeruntime;
+
+/**
+ * Native methods for android.graphics.fonts.FontFamily JNI registration. This is different from
+ * {@link FontFamilyNatives}.
+ *
+ * <p>Native method signatures are derived from
+ * https://cs.android.com/android/platform/superproject/+/android-12.0.0_r1:frameworks/base/graphics/java/android/graphics/fonts/FontFamily.java
+ */
+public final class FontsFontFamilyNatives {
+
+  public static native int nGetFontSize(long family);
+
+  public static native long nGetFont(long family, int i);
+
+  public static native String nGetLangTags(long family);
+
+  public static native int nGetVariant(long family);
+
+  private FontsFontFamilyNatives() {}
+}
diff --git a/nativeruntime/src/main/java/org/robolectric/nativeruntime/HardwareRendererNatives.java b/nativeruntime/src/main/java/org/robolectric/nativeruntime/HardwareRendererNatives.java
new file mode 100644
index 0000000..8553ecc
--- /dev/null
+++ b/nativeruntime/src/main/java/org/robolectric/nativeruntime/HardwareRendererNatives.java
@@ -0,0 +1,173 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.robolectric.nativeruntime;
+
+import android.graphics.Bitmap;
+import android.graphics.HardwareRenderer.ASurfaceTransactionCallback;
+import android.graphics.HardwareRenderer.FrameCompleteCallback;
+import android.graphics.HardwareRenderer.FrameDrawingCallback;
+import android.graphics.HardwareRenderer.PictureCapturedCallback;
+import android.graphics.HardwareRenderer.PrepareSurfaceControlForWebviewCallback;
+import android.view.Surface;
+import java.io.FileDescriptor;
+
+/**
+ * Native methods for {@link HardwareRenderer} JNI registration.
+ *
+ * <p>Native method signatures are derived from
+ * https://cs.android.com/android/platform/superproject/+/android-12.0.0_r1:frameworks/base/graphics/java/android/graphics/HardwareRenderer.java
+ */
+public final class HardwareRendererNatives {
+  public static native void disableVsync();
+
+  public static native void preload();
+
+  public static native boolean isWebViewOverlaysEnabled();
+
+  public static native void setupShadersDiskCache(String cacheFile, String skiaCacheFile);
+
+  public static native void nRotateProcessStatsBuffer();
+
+  public static native void nSetProcessStatsBuffer(int fd);
+
+  public static native int nGetRenderThreadTid(long nativeProxy);
+
+  public static native long nCreateRootRenderNode();
+
+  public static native long nCreateProxy(boolean translucent, long rootRenderNode);
+
+  public static native void nDeleteProxy(long nativeProxy);
+
+  public static native boolean nLoadSystemProperties(long nativeProxy);
+
+  public static native void nSetName(long nativeProxy, String name);
+
+  public static native void nSetSurface(long nativeProxy, Surface window, boolean discardBuffer);
+
+  public static native void nSetSurfaceControl(long nativeProxy, long nativeSurfaceControl);
+
+  public static native boolean nPause(long nativeProxy);
+
+  public static native void nSetStopped(long nativeProxy, boolean stopped);
+
+  public static native void nSetLightGeometry(
+      long nativeProxy, float lightX, float lightY, float lightZ, float lightRadius);
+
+  public static native void nSetLightAlpha(
+      long nativeProxy, float ambientShadowAlpha, float spotShadowAlpha);
+
+  public static native void nSetOpaque(long nativeProxy, boolean opaque);
+
+  public static native void nSetColorMode(long nativeProxy, int colorMode);
+
+  public static native void nSetSdrWhitePoint(long nativeProxy, float whitePoint);
+
+  public static native void nSetIsHighEndGfx(boolean isHighEndGfx);
+
+  public static native int nSyncAndDrawFrame(long nativeProxy, long[] frameInfo, int size);
+
+  public static native void nDestroy(long nativeProxy, long rootRenderNode);
+
+  public static native void nRegisterAnimatingRenderNode(long rootRenderNode, long animatingNode);
+
+  public static native void nRegisterVectorDrawableAnimator(long rootRenderNode, long animator);
+
+  public static native long nCreateTextureLayer(long nativeProxy);
+
+  public static native void nBuildLayer(long nativeProxy, long node);
+
+  public static native boolean nCopyLayerInto(long nativeProxy, long layer, long bitmapHandle);
+
+  public static native void nPushLayerUpdate(long nativeProxy, long layer);
+
+  public static native void nCancelLayerUpdate(long nativeProxy, long layer);
+
+  public static native void nDetachSurfaceTexture(long nativeProxy, long layer);
+
+  public static native void nDestroyHardwareResources(long nativeProxy);
+
+  public static native void nTrimMemory(int level);
+
+  public static native void nOverrideProperty(String name, String value);
+
+  public static native void nFence(long nativeProxy);
+
+  public static native void nStopDrawing(long nativeProxy);
+
+  public static native void nNotifyFramePending(long nativeProxy);
+
+  public static native void nDumpProfileInfo(long nativeProxy, FileDescriptor fd, int dumpFlags);
+
+  public static native void nAddRenderNode(
+      long nativeProxy, long rootRenderNode, boolean placeFront);
+
+  public static native void nRemoveRenderNode(long nativeProxy, long rootRenderNode);
+
+  public static native void nDrawRenderNode(long nativeProxy, long rootRenderNode);
+
+  public static native void nSetContentDrawBounds(
+      long nativeProxy, int left, int top, int right, int bottom);
+
+  public static native void nSetPictureCaptureCallback(
+      long nativeProxy, PictureCapturedCallback callback);
+
+  public static native void nSetASurfaceTransactionCallback(
+      long nativeProxy, ASurfaceTransactionCallback callback);
+
+  public static native void nSetPrepareSurfaceControlForWebviewCallback(
+      long nativeProxy, PrepareSurfaceControlForWebviewCallback callback);
+
+  public static native void nSetFrameCallback(long nativeProxy, FrameDrawingCallback callback);
+
+  public static native void nSetFrameCompleteCallback(
+      long nativeProxy, FrameCompleteCallback callback);
+
+  public static native void nAddObserver(long nativeProxy, long nativeObserver);
+
+  public static native void nRemoveObserver(long nativeProxy, long nativeObserver);
+
+  public static native int nCopySurfaceInto(
+      Surface surface, int srcLeft, int srcTop, int srcRight, int srcBottom, long bitmapHandle);
+
+  public static native Bitmap nCreateHardwareBitmap(long renderNode, int width, int height);
+
+  public static native void nSetHighContrastText(boolean enabled);
+
+  public static native void nHackySetRTAnimationsEnabled(boolean enabled);
+
+  public static native void nSetDebuggingEnabled(boolean enabled);
+
+  public static native void nSetIsolatedProcess(boolean enabled);
+
+  public static native void nSetContextPriority(int priority);
+
+  public static native void nAllocateBuffers(long nativeProxy);
+
+  public static native void nSetForceDark(long nativeProxy, boolean enabled);
+
+  public static native void nSetDisplayDensityDpi(int densityDpi);
+
+  public static native void nInitDisplayInfo(
+      int width,
+      int height,
+      float refreshRate,
+      int wideColorDataspace,
+      long appVsyncOffsetNanos,
+      long presentationDeadlineNanos);
+
+  private HardwareRendererNatives() {}
+}
diff --git a/nativeruntime/src/main/java/org/robolectric/nativeruntime/HardwareRendererObserverNatives.java b/nativeruntime/src/main/java/org/robolectric/nativeruntime/HardwareRendererObserverNatives.java
new file mode 100644
index 0000000..4de5d34
--- /dev/null
+++ b/nativeruntime/src/main/java/org/robolectric/nativeruntime/HardwareRendererObserverNatives.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.robolectric.nativeruntime;
+
+/**
+ * Native methods for {@link ImageDecoder} JNI registration.
+ *
+ * <p>Native method signatures are derived from
+ * https://cs.android.com/android/platform/superproject/+/android-12.0.0_r1:frameworks/base/graphics/java/android/graphics/HardwareRendererObserver.java
+ */
+public class HardwareRendererObserverNatives {
+  public static native int nGetNextBuffer(long nativePtr, long[] data);
+
+  public native long nCreateObserver(boolean waitForPresentTime);
+}
diff --git a/nativeruntime/src/main/java/org/robolectric/nativeruntime/ImageDecoderNatives.java b/nativeruntime/src/main/java/org/robolectric/nativeruntime/ImageDecoderNatives.java
new file mode 100644
index 0000000..36d4925
--- /dev/null
+++ b/nativeruntime/src/main/java/org/robolectric/nativeruntime/ImageDecoderNatives.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.robolectric.nativeruntime;
+
+import android.graphics.Bitmap;
+import android.graphics.ColorSpace;
+import android.graphics.ImageDecoder;
+import android.graphics.ImageDecoder.Source;
+import android.graphics.Rect;
+import android.util.Size;
+import java.io.FileDescriptor;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.ByteBuffer;
+
+/**
+ * Native methods for {@link ImageDecoder} JNI registration.
+ *
+ * <p>Native method signatures are derived from
+ * https://cs.android.com/android/platform/superproject/+/android-12.0.0_r1:frameworks/base/graphics/java/android/graphics/ImageDecoder.java
+ */
+public final class ImageDecoderNatives {
+
+  public static native ImageDecoder nCreate(long asset, boolean preferAnimation, Source src)
+      throws IOException;
+
+  public static native ImageDecoder nCreate(
+      ByteBuffer buffer, int position, int limit, boolean preferAnimation, Source src)
+      throws IOException;
+
+  public static native ImageDecoder nCreate(
+      byte[] data, int offset, int length, boolean preferAnimation, Source src) throws IOException;
+
+  public static native ImageDecoder nCreate(
+      InputStream is, byte[] storage, boolean preferAnimation, Source src) throws IOException;
+  // The fd must be seekable.
+  public static native ImageDecoder nCreate(
+      FileDescriptor fd, long length, boolean preferAnimation, Source src) throws IOException;
+
+  public static native Bitmap nDecodeBitmap(
+      long nativePtr,
+      ImageDecoder decoder,
+      boolean doPostProcess,
+      int width,
+      int height,
+      Rect cropRect,
+      boolean mutable,
+      int allocator,
+      boolean unpremulRequired,
+      boolean conserveMemory,
+      boolean decodeAsAlphaMask,
+      long desiredColorSpace,
+      boolean extended)
+      throws IOException;
+
+  public static native Size nGetSampledSize(long nativePtr, int sampleSize);
+
+  public static native void nGetPadding(long nativePtr, Rect outRect);
+
+  public static native void nClose(long nativePtr);
+
+  public static native String nGetMimeType(long nativePtr);
+
+  public static native ColorSpace nGetColorSpace(long nativePtr);
+
+  private ImageDecoderNatives() {}
+}
diff --git a/nativeruntime/src/main/java/org/robolectric/nativeruntime/InterpolatorNatives.java b/nativeruntime/src/main/java/org/robolectric/nativeruntime/InterpolatorNatives.java
new file mode 100644
index 0000000..d923579
--- /dev/null
+++ b/nativeruntime/src/main/java/org/robolectric/nativeruntime/InterpolatorNatives.java
@@ -0,0 +1,25 @@
+package org.robolectric.nativeruntime;
+
+/**
+ * Native methods for Interpolator JNI registration.
+ *
+ * <p>Native method signatures are derived from
+ * https://cs.android.com/android/platform/superproject/+/android-12.0.0_r1:frameworks/base/graphics/java/android/graphics/Interpolator.java
+ */
+public final class InterpolatorNatives {
+  public static native long nativeConstructor(int valueCount, int frameCount);
+
+  public static native void nativeDestructor(long nativeInstance);
+
+  public static native void nativeReset(long nativeInstance, int valueCount, int frameCount);
+
+  public static native void nativeSetKeyFrame(
+      long nativeInstance, int index, int msec, float[] values, float[] blend);
+
+  public static native void nativeSetRepeatMirror(
+      long nativeInstance, float repeatCount, boolean mirror);
+
+  public static native int nativeTimeToValues(long nativeInstance, int msec, float[] values);
+
+  private InterpolatorNatives() {}
+}
diff --git a/nativeruntime/src/main/java/org/robolectric/nativeruntime/LightingColorFilterNatives.java b/nativeruntime/src/main/java/org/robolectric/nativeruntime/LightingColorFilterNatives.java
new file mode 100644
index 0000000..264a0c3
--- /dev/null
+++ b/nativeruntime/src/main/java/org/robolectric/nativeruntime/LightingColorFilterNatives.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2006 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.robolectric.nativeruntime;
+
+/**
+ * Native methods for LightingColorFilter JNI registration.
+ *
+ * <p>Native method signatures are derived from
+ * https://cs.android.com/android/platform/superproject/+/android-12.0.0_r1:frameworks/base/graphics/java/android/graphics/LightingColorFilter.java
+ */
+public final class LightingColorFilterNatives {
+
+  public static native long native_CreateLightingFilter(int mul, int add);
+
+  private LightingColorFilterNatives() {}
+}
diff --git a/nativeruntime/src/main/java/org/robolectric/nativeruntime/LineBreakerNatives.java b/nativeruntime/src/main/java/org/robolectric/nativeruntime/LineBreakerNatives.java
new file mode 100644
index 0000000..dbf256e
--- /dev/null
+++ b/nativeruntime/src/main/java/org/robolectric/nativeruntime/LineBreakerNatives.java
@@ -0,0 +1,87 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.robolectric.nativeruntime;
+
+import android.annotation.FloatRange;
+import android.annotation.IntRange;
+
+/**
+ * Native methods for LineBreaker JNI registration.
+ *
+ * <p>Native method signatures are derived from
+ * https://cs.android.com/android/platform/superproject/+/android-12.0.0_r1:frameworks/base/graphics/java/android/graphics/text/LineBreaker.java
+ */
+public final class LineBreakerNatives {
+  public static native long nInit(
+      int breakStrategy, int hyphenationFrequency, boolean isJustified, int[] indents);
+
+  public static native long nGetReleaseFunc();
+
+  public static native long nComputeLineBreaks(
+      long nativePtr,
+      char[] text,
+      long measuredTextPtr,
+      @IntRange(from = 0) int length,
+      @FloatRange(from = 0.0f) float firstWidth,
+      @IntRange(from = 0) int firstWidthLineCount,
+      @FloatRange(from = 0.0f) float restWidth,
+      float[] variableTabStops,
+      float defaultTabStop,
+      @IntRange(from = 0) int indentsOffset);
+
+  public static native int nComputeLineBreaksP(
+      /* non zero */ long nativePtr,
+
+      // Inputs
+      char[] text,
+      /* Non Zero */ long measuredTextPtr,
+      @IntRange(from = 0) int length,
+      @FloatRange(from = 0.0f) float firstWidth,
+      @IntRange(from = 0) int firstWidthLineCount,
+      @FloatRange(from = 0.0f) float restWidth,
+      float[] variableTabStops,
+      float defaultTabStop,
+      @IntRange(from = 0) int indentsOffset,
+
+      // Outputs
+      /* LineBreaks */ Object recycle,
+      @IntRange(from = 0) int recycleLength,
+      int[] recycleBreaks,
+      float[] recycleWidths,
+      float[] recycleAscents,
+      float[] recycleDescents,
+      int[] recycleFlags,
+      float[] charWidths);
+
+  public static native int nGetLineCount(long ptr);
+
+  public static native int nGetLineBreakOffset(long ptr, int idx);
+
+  public static native float nGetLineWidth(long ptr, int idx);
+
+  public static native float nGetLineAscent(long ptr, int idx);
+
+  public static native float nGetLineDescent(long ptr, int idx);
+
+  public static native int nGetLineFlag(long ptr, int idx);
+
+  public static native long nGetReleaseResultFunc();
+
+  public static native void nFinishP(long nativePtr);
+
+  private LineBreakerNatives() {}
+}
diff --git a/nativeruntime/src/main/java/org/robolectric/nativeruntime/LinearGradientNatives.java b/nativeruntime/src/main/java/org/robolectric/nativeruntime/LinearGradientNatives.java
new file mode 100644
index 0000000..84b1bc8
--- /dev/null
+++ b/nativeruntime/src/main/java/org/robolectric/nativeruntime/LinearGradientNatives.java
@@ -0,0 +1,35 @@
+package org.robolectric.nativeruntime;
+
+/**
+ * Native methods for LinearGradient JNI registration.
+ *
+ * <p>Native method signatures are derived from
+ * https://cs.android.com/android/platform/superproject/+/android-12.0.0_r1:frameworks/base/graphics/java/android/graphics/LinearGradient.java
+ */
+public final class LinearGradientNatives {
+  public static native long nativeCreate(
+      long matrix,
+      float x0,
+      float y0,
+      float x1,
+      float y1,
+      long[] colors,
+      float[] positions,
+      int tileMode,
+      long colorSpaceHandle);
+
+  public static native long nativeCreate1(
+      long matrix,
+      float x0,
+      float y0,
+      float x1,
+      float y1,
+      int[] colors,
+      float[] positions,
+      int tileMode);
+
+  public static native long nativeCreate2(
+      long matrix, float x0, float y0, float x1, float y1, int color0, int color1, int tileMode);
+
+  private LinearGradientNatives() {}
+}
diff --git a/nativeruntime/src/main/java/org/robolectric/nativeruntime/MaskFilterNatives.java b/nativeruntime/src/main/java/org/robolectric/nativeruntime/MaskFilterNatives.java
new file mode 100644
index 0000000..6830351
--- /dev/null
+++ b/nativeruntime/src/main/java/org/robolectric/nativeruntime/MaskFilterNatives.java
@@ -0,0 +1,14 @@
+package org.robolectric.nativeruntime;
+
+/**
+ * Native methods for MaskFilter JNI registration.
+ *
+ * <p>Native method signatures are derived from
+ * https://cs.android.com/android/platform/superproject/+/android-12.0.0_r1:frameworks/base/graphics/java/android/graphics/MaskFilter.java
+ */
+public final class MaskFilterNatives {
+
+  public static native void nativeDestructor(long nativeFilter);
+
+  private MaskFilterNatives() {}
+}
diff --git a/nativeruntime/src/main/java/org/robolectric/nativeruntime/MatrixNatives.java b/nativeruntime/src/main/java/org/robolectric/nativeruntime/MatrixNatives.java
new file mode 100644
index 0000000..d3b873a
--- /dev/null
+++ b/nativeruntime/src/main/java/org/robolectric/nativeruntime/MatrixNatives.java
@@ -0,0 +1,123 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.robolectric.nativeruntime;
+
+import android.graphics.RectF;
+
+/**
+ * Native methods for Matrix JNI registration.
+ *
+ * <p>Native method signatures are derived from
+ * https://cs.android.com/android/platform/superproject/+/android-12.0.0_r1:frameworks/base/graphics/java/android/graphics/Matrix.java
+ */
+public class MatrixNatives {
+
+  public static native long nCreate(long nSrcOrZero);
+
+  public static native long nGetNativeFinalizer();
+
+  public static native boolean nSetRectToRect(long nObject, RectF src, RectF dst, int stf);
+
+  public static native boolean nSetPolyToPoly(
+      long nObject, float[] src, int srcIndex, float[] dst, int dstIndex, int pointCount);
+
+  public static native void nMapPoints(
+      long nObject,
+      float[] dst,
+      int dstIndex,
+      float[] src,
+      int srcIndex,
+      int ptCount,
+      boolean isPts);
+
+  public static native boolean nMapRect(long nObject, RectF dst, RectF src);
+
+  public static native void nGetValues(long nObject, float[] values);
+
+  public static native void nSetValues(long nObject, float[] values);
+
+  // ------------------ Critical JNI ------------------------
+
+  public static native boolean nIsIdentity(long nObject);
+
+  public static native boolean nIsAffine(long nObject);
+
+  public static native boolean nRectStaysRect(long nObject);
+
+  public static native void nReset(long nObject);
+
+  public static native void nSet(long nObject, long nOther);
+
+  public static native void nSetTranslate(long nObject, float dx, float dy);
+
+  public static native void nSetScale(long nObject, float sx, float sy, float px, float py);
+
+  public static native void nSetScale(long nObject, float sx, float sy);
+
+  public static native void nSetRotate(long nObject, float degrees, float px, float py);
+
+  public static native void nSetRotate(long nObject, float degrees);
+
+  public static native void nSetSinCos(
+      long nObject, float sinValue, float cosValue, float px, float py);
+
+  public static native void nSetSinCos(long nObject, float sinValue, float cosValue);
+
+  public static native void nSetSkew(long nObject, float kx, float ky, float px, float py);
+
+  public static native void nSetSkew(long nObject, float kx, float ky);
+
+  public static native void nSetConcat(long nObject, long nA, long nB);
+
+  public static native void nPreTranslate(long nObject, float dx, float dy);
+
+  public static native void nPreScale(long nObject, float sx, float sy, float px, float py);
+
+  public static native void nPreScale(long nObject, float sx, float sy);
+
+  public static native void nPreRotate(long nObject, float degrees, float px, float py);
+
+  public static native void nPreRotate(long nObject, float degrees);
+
+  public static native void nPreSkew(long nObject, float kx, float ky, float px, float py);
+
+  public static native void nPreSkew(long nObject, float kx, float ky);
+
+  public static native void nPreConcat(long nObject, long nOtherMatrix);
+
+  public static native void nPostTranslate(long nObject, float dx, float dy);
+
+  public static native void nPostScale(long nObject, float sx, float sy, float px, float py);
+
+  public static native void nPostScale(long nObject, float sx, float sy);
+
+  public static native void nPostRotate(long nObject, float degrees, float px, float py);
+
+  public static native void nPostRotate(long nObject, float degrees);
+
+  public static native void nPostSkew(long nObject, float kx, float ky, float px, float py);
+
+  public static native void nPostSkew(long nObject, float kx, float ky);
+
+  public static native void nPostConcat(long nObject, long nOtherMatrix);
+
+  public static native boolean nInvert(long nObject, long nInverse);
+
+  public static native float nMapRadius(long nObject, float radius);
+
+  public static native boolean nEquals(long nA, long nB);
+}
diff --git a/nativeruntime/src/main/java/org/robolectric/nativeruntime/MeasuredTextBuilderNatives.java b/nativeruntime/src/main/java/org/robolectric/nativeruntime/MeasuredTextBuilderNatives.java
new file mode 100644
index 0000000..2ea75a2
--- /dev/null
+++ b/nativeruntime/src/main/java/org/robolectric/nativeruntime/MeasuredTextBuilderNatives.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.robolectric.nativeruntime;
+
+import android.annotation.FloatRange;
+import android.annotation.IntRange;
+
+/**
+ * Native methods for MeasuredText.Builder JNI registration.
+ *
+ * <p>Native method signatures are derived from
+ * https://cs.android.com/android/platform/superproject/+/android-12.0.0_r1:frameworks/base/graphics/java/android/graphics/text/MeasuredText.java
+ */
+public final class MeasuredTextBuilderNatives {
+
+  public static native /* Non Zero */ long nInitBuilder();
+
+  public static native void nAddStyleRun(
+      /* Non Zero */ long nativeBuilderPtr,
+      /* Non Zero */ long paintPtr,
+      @IntRange(from = 0) int start,
+      @IntRange(from = 0) int end,
+      boolean isRtl);
+
+  public static native void nAddReplacementRun(
+      /* Non Zero */ long nativeBuilderPtr,
+      /* Non Zero */ long paintPtr,
+      @IntRange(from = 0) int start,
+      @IntRange(from = 0) int end,
+      @FloatRange(from = 0) float width);
+
+  public static native long nBuildMeasuredText(
+      /* Non Zero */ long nativeBuilderPtr,
+      long hintMtPtr,
+      char[] text,
+      boolean computeHyphenation,
+      boolean computeLayout);
+
+  public static native void nFreeBuilder(/* Non Zero */ long nativeBuilderPtr);
+
+  private MeasuredTextBuilderNatives() {}
+}
diff --git a/nativeruntime/src/main/java/org/robolectric/nativeruntime/MeasuredTextNatives.java b/nativeruntime/src/main/java/org/robolectric/nativeruntime/MeasuredTextNatives.java
new file mode 100644
index 0000000..5f12a0d
--- /dev/null
+++ b/nativeruntime/src/main/java/org/robolectric/nativeruntime/MeasuredTextNatives.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.robolectric.nativeruntime;
+
+import android.annotation.IntRange;
+import android.graphics.Rect;
+
+/**
+ * Native methods for MeasuredText JNI registration.
+ *
+ * <p>Native method signatures are derived from
+ * https://cs.android.com/android/platform/superproject/+/android-12.0.0_r1:frameworks/base/graphics/java/android/graphics/text/MeasuredText.java
+ */
+public final class MeasuredTextNatives {
+
+  public static native float nGetWidth(
+      /* Non Zero */ long nativePtr, @IntRange(from = 0) int start, @IntRange(from = 0) int end);
+
+  public static native /* Non Zero */ long nGetReleaseFunc();
+
+  public static native int nGetMemoryUsage(/* Non Zero */ long nativePtr);
+
+  public static native void nGetBounds(long nativePtr, char[] buf, int start, int end, Rect rect);
+
+  public static native float nGetCharWidthAt(long nativePtr, int offset);
+
+  private MeasuredTextNatives() {}
+}
diff --git a/nativeruntime/src/main/java/org/robolectric/nativeruntime/NIOAccess.java b/nativeruntime/src/main/java/org/robolectric/nativeruntime/NIOAccess.java
new file mode 100644
index 0000000..cc20e26
--- /dev/null
+++ b/nativeruntime/src/main/java/org/robolectric/nativeruntime/NIOAccess.java
@@ -0,0 +1,100 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.robolectric.nativeruntime;
+
+import static org.robolectric.util.reflector.Reflector.reflector;
+
+import java.nio.Buffer;
+import java.nio.ByteBuffer;
+import java.nio.CharBuffer;
+import java.nio.DoubleBuffer;
+import java.nio.FloatBuffer;
+import java.nio.IntBuffer;
+import java.nio.LongBuffer;
+import java.nio.ShortBuffer;
+import org.robolectric.util.reflector.Accessor;
+import org.robolectric.util.reflector.ForType;
+
+/**
+ * Analogue to libcore's <a
+ * href="https://cs.android.com/android/platform/superproject/+/android-12.0.0_r1:libcore/luni/src/main/java/java/nio/NIOAccess.java">NIOAccess</a>,
+ * which provides access to some internal methods and properties of {@link Buffer}. These methods
+ * are designed to work on the JVM and get called from native code such as libnativehelper.
+ */
+public final class NIOAccess {
+
+  private NIOAccess() {}
+
+  /**
+   * Returns the underlying native pointer to the data of the given Buffer starting at the Buffer's
+   * current position, or 0 if the Buffer is not backed by native heap storage.
+   */
+  public static long getBasePointer(Buffer b) {
+    long address = reflector(BufferReflector.class, b).getAddress();
+
+    if (address == 0L || !b.isDirect()) {
+      return 0L;
+    }
+    return address + ((long) b.position() << elementSizeShift(b));
+  }
+
+  /**
+   * Returns the underlying Java array containing the data of the given Buffer, or null if the
+   * Buffer is not backed by a Java array.
+   */
+  static Object getBaseArray(Buffer b) {
+    return b.hasArray() ? b.array() : null;
+  }
+
+  /**
+   * Returns the offset in bytes from the start of the underlying Java array object containing the
+   * data of the given Buffer to the actual start of the data. The start of the data takes into
+   * account the Buffer's current position. This method is only meaningful if getBaseArray() returns
+   * non-null.
+   */
+  static int getBaseArrayOffset(Buffer b) {
+    return b.hasArray() ? ((b.arrayOffset() + b.position()) << elementSizeShift(b)) : 0;
+  }
+
+  /**
+   * The Android version of java.nio.Buffer has an extra final field called _elementSizeShift that
+   * only depend on the implementation of the buffer. This method can be called instead when wanting
+   * to access the value of that field on the JVM.
+   */
+  public static int elementSizeShift(Buffer buffer) {
+    if (buffer instanceof ByteBuffer) {
+      return 0;
+    }
+    if (buffer instanceof ShortBuffer || buffer instanceof CharBuffer) {
+      return 1;
+    }
+    if (buffer instanceof IntBuffer || buffer instanceof FloatBuffer) {
+      return 2;
+    }
+    if (buffer instanceof LongBuffer || buffer instanceof DoubleBuffer) {
+      return 3;
+    }
+    return 0;
+  }
+
+  @ForType(Buffer.class)
+  interface BufferReflector {
+
+    @Accessor("address")
+    long getAddress();
+  }
+}
diff --git a/nativeruntime/src/main/java/org/robolectric/nativeruntime/NativeAllocationRegistryNatives.java b/nativeruntime/src/main/java/org/robolectric/nativeruntime/NativeAllocationRegistryNatives.java
new file mode 100644
index 0000000..18a2a35
--- /dev/null
+++ b/nativeruntime/src/main/java/org/robolectric/nativeruntime/NativeAllocationRegistryNatives.java
@@ -0,0 +1,13 @@
+package org.robolectric.nativeruntime;
+
+/**
+ * Native methods for NativeAllocationRegistry JNI registration.
+ *
+ * <p>Native method signatures are derived from
+ * https://cs.android.com/android/platform/superproject/+/android-12.0.0_r1:libcore/luni/src/main/java/libcore/util/NativeAllocationRegistry.java
+ */
+public final class NativeAllocationRegistryNatives {
+  public static native void applyFreeFunction(long freeFunction, long nativePtr);
+
+  private NativeAllocationRegistryNatives() {}
+}
diff --git a/nativeruntime/src/main/java/org/robolectric/nativeruntime/NativeInterpolatorFactoryNatives.java b/nativeruntime/src/main/java/org/robolectric/nativeruntime/NativeInterpolatorFactoryNatives.java
new file mode 100644
index 0000000..728cb99
--- /dev/null
+++ b/nativeruntime/src/main/java/org/robolectric/nativeruntime/NativeInterpolatorFactoryNatives.java
@@ -0,0 +1,34 @@
+package org.robolectric.nativeruntime;
+
+/**
+ * Native methods for NativeInterpolatorFactory JNI registration.
+ *
+ * <p>Native method signatures are derived from
+ * https://cs.android.com/android/platform/superproject/+/android-12.0.0_r1:frameworks/base/graphics/java/android/graphics/NativeInterpolatorFactory.java
+ */
+public final class NativeInterpolatorFactoryNatives {
+
+  public static native long createAccelerateDecelerateInterpolator();
+
+  public static native long createAccelerateInterpolator(float factor);
+
+  public static native long createAnticipateInterpolator(float tension);
+
+  public static native long createAnticipateOvershootInterpolator(float tension);
+
+  public static native long createBounceInterpolator();
+
+  public static native long createCycleInterpolator(float cycles);
+
+  public static native long createDecelerateInterpolator(float factor);
+
+  public static native long createLinearInterpolator();
+
+  public static native long createOvershootInterpolator(float tension);
+
+  public static native long createPathInterpolator(float[] x, float[] y);
+
+  public static native long createLutInterpolator(float[] values);
+
+  private NativeInterpolatorFactoryNatives() {}
+}
diff --git a/nativeruntime/src/main/java/org/robolectric/nativeruntime/NinePatchNatives.java b/nativeruntime/src/main/java/org/robolectric/nativeruntime/NinePatchNatives.java
new file mode 100644
index 0000000..f76a298
--- /dev/null
+++ b/nativeruntime/src/main/java/org/robolectric/nativeruntime/NinePatchNatives.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.robolectric.nativeruntime;
+
+import android.graphics.Rect;
+
+/**
+ * Native methods for NinePatch JNI registration.
+ *
+ * <p>Native method signatures are derived from
+ * https://cs.android.com/android/platform/superproject/+/android-12.0.0_r1:frameworks/base/graphics/java/android/graphics/NinePatch.java
+ */
+public final class NinePatchNatives {
+
+  public static native boolean isNinePatchChunk(byte[] chunk);
+
+  public static native long validateNinePatchChunk(byte[] chunk);
+
+  public static native void nativeFinalize(long chunk);
+
+  public static native long nativeGetTransparentRegion(
+      long bitmapHandle, long chunk, Rect location);
+
+  private NinePatchNatives() {}
+}
diff --git a/nativeruntime/src/main/java/org/robolectric/nativeruntime/PaintNatives.java b/nativeruntime/src/main/java/org/robolectric/nativeruntime/PaintNatives.java
new file mode 100644
index 0000000..b803e1c
--- /dev/null
+++ b/nativeruntime/src/main/java/org/robolectric/nativeruntime/PaintNatives.java
@@ -0,0 +1,442 @@
+/*
+ * Copyright (C) 2006 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.robolectric.nativeruntime;
+
+import android.annotation.ColorInt;
+import android.annotation.ColorLong;
+import android.graphics.Paint.FontMetrics;
+import android.graphics.Paint.FontMetricsInt;
+import android.graphics.Rect;
+
+/**
+ * Native methods for Paint JNI registration.
+ *
+ * <p>Native method signatures are derived from
+ * https://cs.android.com/android/platform/superproject/+/android-12.0.0_r1:frameworks/base/graphics/java/android/graphics/Paint.java
+ */
+public final class PaintNatives {
+
+  public static native long nGetNativeFinalizer();
+
+  public static native long nInit();
+
+  public static native long nInitWithPaint(long paint);
+
+  public static native int nBreakText(
+      long nObject,
+      char[] text,
+      int index,
+      int count,
+      float maxWidth,
+      int bidiFlags,
+      float[] measuredWidth);
+
+  public static native int nBreakText(
+      long nObject,
+      String text,
+      boolean measureForwards,
+      float maxWidth,
+      int bidiFlags,
+      float[] measuredWidth);
+
+  public static native int nBreakText(
+      long nObject,
+      long typefacePtr,
+      char[] text,
+      int index,
+      int count,
+      float maxWidth,
+      int bidiFlags,
+      float[] measuredWidth);
+
+  public static native int nBreakText(
+      long nObject,
+      long typefacePtr,
+      String text,
+      boolean measureForwards,
+      float maxWidth,
+      int bidiFlags,
+      float[] measuredWidth);
+
+  public static native int nGetColor(long paintPtr);
+
+  public static native int nGetAlpha(long paintPtr);
+
+  public static native float nGetTextAdvances(
+      long paintPtr,
+      long typefacePtr,
+      char[] text,
+      int index,
+      int count,
+      int contextIndex,
+      int contextCount,
+      int bidiFlags,
+      float[] advances,
+      int advancesIndex);
+
+  public static native float nGetTextAdvances(
+      long paintPtr,
+      long typefacePtr,
+      String text,
+      int start,
+      int end,
+      int contextStart,
+      int contextEnd,
+      int bidiFlags,
+      float[] advances,
+      int advancesIndex);
+
+  public static native float nGetTextAdvances(
+      long paintPtr,
+      char[] text,
+      int index,
+      int count,
+      int contextIndex,
+      int contextCount,
+      int bidiFlags,
+      float[] advances,
+      int advancesIndex);
+
+  public static native float nGetTextAdvances(
+      long paintPtr,
+      String text,
+      int start,
+      int end,
+      int contextStart,
+      int contextEnd,
+      int bidiFlags,
+      float[] advances,
+      int advancesIndex);
+
+  public native int nGetTextRunCursor(
+      long paintPtr,
+      char[] text,
+      int contextStart,
+      int contextLength,
+      int dir,
+      int offset,
+      int cursorOpt);
+
+  public native int nGetTextRunCursor(
+      long paintPtr,
+      String text,
+      int contextStart,
+      int contextEnd,
+      int dir,
+      int offset,
+      int cursorOpt);
+
+  public native int nGetTextRunCursor(
+      long paintPtr,
+      long typefacePtr,
+      char[] text,
+      int contextStart,
+      int contextLength,
+      int dir,
+      int offset,
+      int cursorOpt);
+
+  public native int nGetTextRunCursor(
+      long paintPtr,
+      long typefacePtr,
+      String text,
+      int contextStart,
+      int contextEnd,
+      int dir,
+      int offset,
+      int cursorOpt);
+
+  public static native void nGetTextPath(
+      long paintPtr, int bidiFlags, char[] text, int index, int count, float x, float y, long path);
+
+  public static native void nGetTextPath(
+      long paintPtr, int bidiFlags, String text, int start, int end, float x, float y, long path);
+
+  public static native void nGetTextPath(
+      long paintPtr,
+      long typefacePtr,
+      int bidiFlags,
+      char[] text,
+      int index,
+      int count,
+      float x,
+      float y,
+      long path);
+
+  public static native void nGetTextPath(
+      long paintPtr,
+      long typefacePtr,
+      int bidiFlags,
+      String text,
+      int start,
+      int end,
+      float x,
+      float y,
+      long path);
+
+  public static native void nGetStringBounds(
+      long nativePaint, String text, int start, int end, int bidiFlags, Rect bounds);
+
+  public static native void nGetStringBounds(
+      long nativePaint,
+      long typefacePtr,
+      String text,
+      int start,
+      int end,
+      int bidiFlags,
+      Rect bounds);
+
+  public static native void nGetCharArrayBounds(
+      long nativePaint, char[] text, int index, int count, int bidiFlags, Rect bounds);
+
+  public static native void nGetCharArrayBounds(
+      long nativePaint,
+      long typefacePtr,
+      char[] text,
+      int index,
+      int count,
+      int bidiFlags,
+      Rect bounds);
+
+  public static native boolean nHasGlyph(long paintPtr, int bidiFlags, String string);
+
+  public static native boolean nHasGlyph(
+      long paintPtr, long typefacePtr, int bidiFlags, String string);
+
+  public static native float nGetRunAdvance(
+      long paintPtr,
+      char[] text,
+      int start,
+      int end,
+      int contextStart,
+      int contextEnd,
+      boolean isRtl,
+      int offset);
+
+  public static native float nGetRunAdvance(
+      long paintPtr,
+      long typefacePtr,
+      char[] text,
+      int start,
+      int end,
+      int contextStart,
+      int contextEnd,
+      boolean isRtl,
+      int offset);
+
+  public static native int nGetOffsetForAdvance(
+      long paintPtr,
+      char[] text,
+      int start,
+      int end,
+      int contextStart,
+      int contextEnd,
+      boolean isRtl,
+      float advance);
+
+  public static native int nGetOffsetForAdvance(
+      long paintPtr,
+      long typefacePtr,
+      char[] text,
+      int start,
+      int end,
+      int contextStart,
+      int contextEnd,
+      boolean isRtl,
+      float advance);
+
+  public static native int nSetTextLocales(long paintPtr, String locales);
+
+  public static native void nSetFontFeatureSettings(long paintPtr, String settings);
+
+  public static native float nGetFontMetrics(long paintPtr, FontMetrics metrics);
+
+  public static native float nGetFontMetrics(long paintPtr, long typefacePtr, FontMetrics metrics);
+
+  public static native int nGetFontMetricsInt(long paintPtr, FontMetricsInt fmi);
+
+  public static native int nGetFontMetricsInt(long paintPtr, long typefacePtr, FontMetricsInt fmi);
+
+  public static native void nReset(long paintPtr);
+
+  public static native void nSet(long paintPtrDest, long paintPtrSrc);
+
+  public static native int nGetStyle(long paintPtr);
+
+  public static native void nSetStyle(long paintPtr, int style);
+
+  public static native int nGetStrokeCap(long paintPtr);
+
+  public static native void nSetStrokeCap(long paintPtr, int cap);
+
+  public static native int nGetStrokeJoin(long paintPtr);
+
+  public static native void nSetStrokeJoin(long paintPtr, int join);
+
+  public static native boolean nGetFillPath(long paintPtr, long src, long dst);
+
+  public static native long nSetShader(long paintPtr, long shader);
+
+  public static native long nSetColorFilter(long paintPtr, long filter);
+
+  public static native void nSetXfermode(long paintPtr, int xfermode);
+
+  public static native long nSetPathEffect(long paintPtr, long effect);
+
+  public static native long nSetMaskFilter(long paintPtr, long maskfilter);
+
+  public static native void nSetTypeface(long paintPtr, long typeface);
+
+  public static native int nGetTextAlign(long paintPtr);
+
+  public static native void nSetTextAlign(long paintPtr, int align);
+
+  public static native void nSetTextLocalesByMinikinLocaleListId(
+      long paintPtr, int mMinikinLocaleListId);
+
+  public static native void nSetShadowLayer(
+      long paintPtr,
+      float radius,
+      float dx,
+      float dy,
+      long colorSpaceHandle,
+      @ColorLong long shadowColor);
+
+  public static native void nSetShadowLayer(
+      long paintPtr, float radius, float dx, float dy, @ColorInt int shadowColor);
+
+  public static native boolean nHasShadowLayer(long paintPtr);
+
+  public static native float nGetLetterSpacing(long paintPtr);
+
+  public static native void nSetLetterSpacing(long paintPtr, float letterSpacing);
+
+  public static native float nGetWordSpacing(long paintPtr);
+
+  public static native void nSetWordSpacing(long paintPtr, float wordSpacing);
+
+  public static native int nGetStartHyphenEdit(long paintPtr);
+
+  public static native int nGetEndHyphenEdit(long paintPtr);
+
+  public static native void nSetStartHyphenEdit(long paintPtr, int hyphen);
+
+  public static native void nSetEndHyphenEdit(long paintPtr, int hyphen);
+
+  public static native void nSetStrokeMiter(long paintPtr, float miter);
+
+  public static native float nGetStrokeMiter(long paintPtr);
+
+  public static native void nSetStrokeWidth(long paintPtr, float width);
+
+  public static native float nGetStrokeWidth(long paintPtr);
+
+  public static native void nSetAlpha(long paintPtr, int a);
+
+  public static native void nSetDither(long paintPtr, boolean dither);
+
+  public static native int nGetFlags(long paintPtr);
+
+  public static native void nSetFlags(long paintPtr, int flags);
+
+  public static native int nGetHinting(long paintPtr);
+
+  public static native void nSetHinting(long paintPtr, int mode);
+
+  public static native void nSetAntiAlias(long paintPtr, boolean aa);
+
+  public static native void nSetLinearText(long paintPtr, boolean linearText);
+
+  public static native void nSetSubpixelText(long paintPtr, boolean subpixelText);
+
+  public static native void nSetUnderlineText(long paintPtr, boolean underlineText);
+
+  public static native void nSetFakeBoldText(long paintPtr, boolean fakeBoldText);
+
+  public static native void nSetFilterBitmap(long paintPtr, boolean filter);
+
+  public static native void nSetColor(long paintPtr, long colorSpaceHandle, @ColorLong long color);
+
+  public static native void nSetColor(long paintPtr, @ColorInt int color);
+
+  public static native void nSetStrikeThruText(long paintPtr, boolean strikeThruText);
+
+  public static native boolean nIsElegantTextHeight(long paintPtr);
+
+  public static native void nSetElegantTextHeight(long paintPtr, boolean elegant);
+
+  public static native float nGetTextSize(long paintPtr);
+
+  public static native float nGetTextScaleX(long paintPtr);
+
+  public static native void nSetTextScaleX(long paintPtr, float scaleX);
+
+  public static native float nGetTextSkewX(long paintPtr);
+
+  public static native void nSetTextSkewX(long paintPtr, float skewX);
+
+  public static native float nAscent(long paintPtr);
+
+  public static native float nAscent(long paintPtr, long typefacePtr);
+
+  public static native float nDescent(long paintPtr);
+
+  public static native float nDescent(long paintPtr, long typefacePtr);
+
+  public static native float nGetUnderlinePosition(long paintPtr);
+
+  public static native float nGetUnderlineThickness(long paintPtr);
+
+  public static native float nGetStrikeThruPosition(long paintPtr);
+
+  public static native float nGetStrikeThruThickness(long paintPtr);
+
+  public static native void nSetTextSize(long paintPtr, float textSize);
+
+  public static native boolean nEqualsForTextMeasurement(long leftPaintPtr, long rightPaintPtr);
+
+  public static native void nGetFontMetricsIntForText(
+      long paintPtr,
+      char[] text,
+      int start,
+      int count,
+      int ctxStart,
+      int ctxCount,
+      boolean isRtl,
+      FontMetricsInt outMetrics);
+
+  public static native void nGetFontMetricsIntForText(
+      long paintPtr,
+      String text,
+      int start,
+      int count,
+      int ctxStart,
+      int ctxCount,
+      boolean isRtl,
+      FontMetricsInt outMetrics);
+
+  public static native float nGetRunCharacterAdvance(
+      long paintPtr,
+      char[] text,
+      int start,
+      int end,
+      int contextStart,
+      int contextEnd,
+      boolean isRtl,
+      int offset,
+      float[] advances,
+      int advancesIndex);
+}
diff --git a/nativeruntime/src/main/java/org/robolectric/nativeruntime/PathDashPathEffectNatives.java b/nativeruntime/src/main/java/org/robolectric/nativeruntime/PathDashPathEffectNatives.java
new file mode 100644
index 0000000..5c508fb
--- /dev/null
+++ b/nativeruntime/src/main/java/org/robolectric/nativeruntime/PathDashPathEffectNatives.java
@@ -0,0 +1,15 @@
+package org.robolectric.nativeruntime;
+
+/**
+ * Native methods for PathDashPathEffect JNI registration.
+ *
+ * <p>Native method signatures are derived from
+ * https://cs.android.com/android/platform/superproject/+/android-12.0.0_r1:frameworks/base/graphics/java/android/graphics/PathDashPathEffect.java
+ */
+public final class PathDashPathEffectNatives {
+
+  public static native long nativeCreate(
+      long nativePath, float advance, float phase, int nativeStyle);
+
+  private PathDashPathEffectNatives() {}
+}
diff --git a/nativeruntime/src/main/java/org/robolectric/nativeruntime/PathEffectNatives.java b/nativeruntime/src/main/java/org/robolectric/nativeruntime/PathEffectNatives.java
new file mode 100644
index 0000000..33215a4
--- /dev/null
+++ b/nativeruntime/src/main/java/org/robolectric/nativeruntime/PathEffectNatives.java
@@ -0,0 +1,14 @@
+package org.robolectric.nativeruntime;
+
+/**
+ * Native methods for PathEffect JNI registration.
+ *
+ * <p>Native method signatures are derived from
+ * https://cs.android.com/android/platform/superproject/+/android-12.0.0_r1:frameworks/base/graphics/java/android/graphics/PathEffect.java
+ */
+public final class PathEffectNatives {
+
+  public static native void nativeDestructor(long nativePatheffect);
+
+  private PathEffectNatives() {}
+}
diff --git a/nativeruntime/src/main/java/org/robolectric/nativeruntime/PathMeasureNatives.java b/nativeruntime/src/main/java/org/robolectric/nativeruntime/PathMeasureNatives.java
new file mode 100644
index 0000000..e1b8d83
--- /dev/null
+++ b/nativeruntime/src/main/java/org/robolectric/nativeruntime/PathMeasureNatives.java
@@ -0,0 +1,34 @@
+package org.robolectric.nativeruntime;
+
+/**
+ * Native methods for PathMeasure JNI registration.
+ *
+ * <p>Native method signatures are derived from
+ * https://cs.android.com/android/platform/superproject/+/android-12.0.0_r1:frameworks/base/graphics/java/android/graphics/PathMeasure.java
+ */
+public final class PathMeasureNatives {
+
+  public static native long native_create(long nativePath, boolean forceClosed);
+
+  public static native void native_setPath(
+      long nativeInstance, long nativePath, boolean forceClosed);
+
+  public static native float native_getLength(long nativeInstance);
+
+  public static native boolean native_getPosTan(
+      long nativeInstance, float distance, float[] pos, float[] tan);
+
+  public static native boolean native_getMatrix(
+      long nativeInstance, float distance, long nativeMatrix, int flags);
+
+  public static native boolean native_getSegment(
+      long nativeInstance, float startD, float stopD, long nativePath, boolean startWithMoveTo);
+
+  public static native boolean native_isClosed(long nativeInstance);
+
+  public static native boolean native_nextContour(long nativeInstance);
+
+  public static native void native_destroy(long nativeInstance);
+
+  private PathMeasureNatives() {}
+}
diff --git a/nativeruntime/src/main/java/org/robolectric/nativeruntime/PathNatives.java b/nativeruntime/src/main/java/org/robolectric/nativeruntime/PathNatives.java
new file mode 100644
index 0000000..0870f6b
--- /dev/null
+++ b/nativeruntime/src/main/java/org/robolectric/nativeruntime/PathNatives.java
@@ -0,0 +1,111 @@
+package org.robolectric.nativeruntime;
+
+import android.graphics.RectF;
+
+/**
+ * Native methods for Path JNI registration.
+ *
+ * <p>Native method signatures are derived from
+ * https://cs.android.com/android/platform/superproject/+/android-12.0.0_r1:frameworks/base/graphics/java/android/graphics/Path.java
+ */
+public final class PathNatives {
+
+  public static native long nInit();
+
+  public static native long nInit(long nPath);
+
+  public static native long nGetFinalizer();
+
+  public static native void nSet(long nativeDst, long nSrc);
+
+  public static native void nComputeBounds(long nPath, RectF bounds);
+
+  public static native void nIncReserve(long nPath, int extraPtCount);
+
+  public static native void nMoveTo(long nPath, float x, float y);
+
+  public static native void nRMoveTo(long nPath, float dx, float dy);
+
+  public static native void nLineTo(long nPath, float x, float y);
+
+  public static native void nRLineTo(long nPath, float dx, float dy);
+
+  public static native void nQuadTo(long nPath, float x1, float y1, float x2, float y2);
+
+  public static native void nRQuadTo(long nPath, float dx1, float dy1, float dx2, float dy2);
+
+  public static native void nCubicTo(
+      long nPath, float x1, float y1, float x2, float y2, float x3, float y3);
+
+  public static native void nRCubicTo(
+      long nPath, float x1, float y1, float x2, float y2, float x3, float y3);
+
+  public static native void nArcTo(
+      long nPath,
+      float left,
+      float top,
+      float right,
+      float bottom,
+      float startAngle,
+      float sweepAngle,
+      boolean forceMoveTo);
+
+  public static native void nClose(long nPath);
+
+  public static native void nAddRect(
+      long nPath, float left, float top, float right, float bottom, int dir);
+
+  public static native void nAddOval(
+      long nPath, float left, float top, float right, float bottom, int dir);
+
+  public static native void nAddCircle(long nPath, float x, float y, float radius, int dir);
+
+  public static native void nAddArc(
+      long nPath,
+      float left,
+      float top,
+      float right,
+      float bottom,
+      float startAngle,
+      float sweepAngle);
+
+  public static native void nAddRoundRect(
+      long nPath, float left, float top, float right, float bottom, float rx, float ry, int dir);
+
+  public static native void nAddRoundRect(
+      long nPath, float left, float top, float right, float bottom, float[] radii, int dir);
+
+  public static native void nAddPath(long nPath, long src, float dx, float dy);
+
+  public static native void nAddPath(long nPath, long src);
+
+  public static native void nAddPath(long nPath, long src, long matrix);
+
+  public static native void nOffset(long nPath, float dx, float dy);
+
+  public static native void nSetLastPoint(long nPath, float dx, float dy);
+
+  public static native void nTransform(long nPath, long matrix, long dstPath);
+
+  public static native void nTransform(long nPath, long matrix);
+
+  public static native boolean nOp(long path1, long path2, int op, long result);
+
+  public static native boolean nIsRect(long nPath, RectF rect);
+
+  public static native void nReset(long nPath);
+
+  public static native void nRewind(long nPath);
+
+  public static native boolean nIsEmpty(long nPath);
+
+  public static native boolean nIsConvex(long nPath);
+
+  public static native int nGetFillType(long nPath);
+
+  public static native void nSetFillType(long nPath, int ft);
+
+  public static native float[] nApproximate(long nPath, float error);
+
+  private PathNatives() {}
+}
diff --git a/nativeruntime/src/main/java/org/robolectric/nativeruntime/PathParserNatives.java b/nativeruntime/src/main/java/org/robolectric/nativeruntime/PathParserNatives.java
new file mode 100644
index 0000000..fb1d5f2
--- /dev/null
+++ b/nativeruntime/src/main/java/org/robolectric/nativeruntime/PathParserNatives.java
@@ -0,0 +1,31 @@
+package org.robolectric.nativeruntime;
+
+/**
+ * Native methods for PathParser JNI registration.
+ *
+ * <p>Native method signatures are derived from
+ * https://cs.android.com/android/platform/superproject/+/android-12.0.0_r1:frameworks/base/graphics/java/android/graphics/PathParser.java
+ */
+public final class PathParserNatives {
+
+  public static native void nParseStringForPath(long pathPtr, String pathString, int stringLength);
+
+  public static native long nCreatePathDataFromString(String pathString, int stringLength);
+
+  public static native void nCreatePathFromPathData(long outPathPtr, long pathData);
+
+  public static native long nCreateEmptyPathData();
+
+  public static native long nCreatePathData(long nativePtr);
+
+  public static native boolean nInterpolatePathData(
+      long outDataPtr, long fromDataPtr, long toDataPtr, float fraction);
+
+  public static native void nFinalize(long nativePtr);
+
+  public static native boolean nCanMorph(long fromDataPtr, long toDataPtr);
+
+  public static native void nSetPathData(long outDataPtr, long fromDataPtr);
+
+  private PathParserNatives() {}
+}
diff --git a/nativeruntime/src/main/java/org/robolectric/nativeruntime/PictureNatives.java b/nativeruntime/src/main/java/org/robolectric/nativeruntime/PictureNatives.java
new file mode 100644
index 0000000..c2bdba6
--- /dev/null
+++ b/nativeruntime/src/main/java/org/robolectric/nativeruntime/PictureNatives.java
@@ -0,0 +1,32 @@
+package org.robolectric.nativeruntime;
+
+import java.io.InputStream;
+import java.io.OutputStream;
+
+/**
+ * Native methods for Picture JNI registration.
+ *
+ * <p>Native method signatures are derived from
+ * https://cs.android.com/android/platform/superproject/+/android-12.0.0_r1:frameworks/base/graphics/java/android/graphics/Picture.java
+ */
+public class PictureNatives {
+
+  public static native long nativeConstructor(long nativeSrcOr0);
+
+  public static native long nativeCreateFromStream(InputStream stream, byte[] storage);
+
+  public static native int nativeGetWidth(long nativePicture);
+
+  public static native int nativeGetHeight(long nativePicture);
+
+  public static native long nativeBeginRecording(long nativeCanvas, int w, int h);
+
+  public static native void nativeEndRecording(long nativeCanvas);
+
+  public static native void nativeDraw(long nativeCanvas, long nativePicture);
+
+  public static native boolean nativeWriteToStream(
+      long nativePicture, OutputStream stream, byte[] storage);
+
+  public static native void nativeDestructor(long nativePicture);
+}
diff --git a/nativeruntime/src/main/java/org/robolectric/nativeruntime/PorterDuffColorFilterNatives.java b/nativeruntime/src/main/java/org/robolectric/nativeruntime/PorterDuffColorFilterNatives.java
new file mode 100644
index 0000000..8071bfd
--- /dev/null
+++ b/nativeruntime/src/main/java/org/robolectric/nativeruntime/PorterDuffColorFilterNatives.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2006 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.robolectric.nativeruntime;
+
+/**
+ * Native methods for PorterDuffColorFilter JNI registration.
+ *
+ * <p>Native method signatures are derived from
+ * https://cs.android.com/android/platform/superproject/+/android-12.0.0_r1:frameworks/base/graphics/java/android/graphics/PorterDuffColorFilter.java
+ */
+public final class PorterDuffColorFilterNatives {
+
+  public static native long native_CreateBlendModeFilter(int srcColor, int blendmode);
+
+  private PorterDuffColorFilterNatives() {}
+}
diff --git a/nativeruntime/src/main/java/org/robolectric/nativeruntime/PropertyValuesHolderNatives.java b/nativeruntime/src/main/java/org/robolectric/nativeruntime/PropertyValuesHolderNatives.java
new file mode 100644
index 0000000..7cc6e01
--- /dev/null
+++ b/nativeruntime/src/main/java/org/robolectric/nativeruntime/PropertyValuesHolderNatives.java
@@ -0,0 +1,41 @@
+package org.robolectric.nativeruntime;
+
+/**
+ * Native methods for PropertyValuesHolder JNI registration.
+ *
+ * <p>Native method signatures are derived from
+ * https://cs.android.com/android/platform/superproject/+/android-12.0.0_r1:frameworks/base/graphics/java/android/graphics/PropertyValuesHolder.java
+ */
+public final class PropertyValuesHolderNatives {
+
+  public static native long nGetIntMethod(Class<?> targetClass, String methodName);
+
+  public static native long nGetFloatMethod(Class<?> targetClass, String methodName);
+
+  public static native long nGetMultipleIntMethod(
+      Class<?> targetClass, String methodName, int numParams);
+
+  public static native long nGetMultipleFloatMethod(
+      Class<?> targetClass, String methodName, int numParams);
+
+  public static native void nCallIntMethod(Object target, long methodID, int arg);
+
+  public static native void nCallFloatMethod(Object target, long methodID, float arg);
+
+  public static native void nCallTwoIntMethod(Object target, long methodID, int arg1, int arg2);
+
+  public static native void nCallFourIntMethod(
+      Object target, long methodID, int arg1, int arg2, int arg3, int arg4);
+
+  public static native void nCallMultipleIntMethod(Object target, long methodID, int[] args);
+
+  public static native void nCallTwoFloatMethod(
+      Object target, long methodID, float arg1, float arg2);
+
+  public static native void nCallFourFloatMethod(
+      Object target, long methodID, float arg1, float arg2, float arg3, float arg4);
+
+  public static native void nCallMultipleFloatMethod(Object target, long methodID, float[] args);
+
+  private PropertyValuesHolderNatives() {}
+}
diff --git a/nativeruntime/src/main/java/org/robolectric/nativeruntime/RadialGradientNatives.java b/nativeruntime/src/main/java/org/robolectric/nativeruntime/RadialGradientNatives.java
new file mode 100644
index 0000000..6c21a81
--- /dev/null
+++ b/nativeruntime/src/main/java/org/robolectric/nativeruntime/RadialGradientNatives.java
@@ -0,0 +1,33 @@
+package org.robolectric.nativeruntime;
+
+import android.annotation.ColorLong;
+
+/**
+ * Native methods for RadialGradient JNI registration.
+ *
+ * <p>Native method signatures are derived from
+ * https://cs.android.com/android/platform/superproject/+/android-12.0.0_r1:frameworks/base/graphics/java/android/graphics/RadialGradient.java
+ */
+public class RadialGradientNatives {
+
+  public static native long nativeCreate(
+      long matrix,
+      float startX,
+      float startY,
+      float startRadius,
+      float endX,
+      float endY,
+      float endRadius,
+      @ColorLong long[] colors,
+      float[] positions,
+      int tileMode,
+      long colorSpaceHandle);
+
+  public static native long nativeCreate1(
+      long matrix, float x, float y, float radius, int[] colors, float[] positions, int tileMode);
+
+  public static native long nativeCreate2(
+      long matrix, float x, float y, float radius, int color0, int color1, int tileMode);
+
+  RadialGradientNatives() {}
+}
diff --git a/nativeruntime/src/main/java/org/robolectric/nativeruntime/RecordingCanvasNatives.java b/nativeruntime/src/main/java/org/robolectric/nativeruntime/RecordingCanvasNatives.java
new file mode 100644
index 0000000..da67153
--- /dev/null
+++ b/nativeruntime/src/main/java/org/robolectric/nativeruntime/RecordingCanvasNatives.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.robolectric.nativeruntime;
+
+/**
+ * Native methods for RecordingCanvas JNI registration.
+ *
+ * <p>Native method signatures are derived from
+ * https://cs.android.com/android/platform/superproject/+/android-12.0.0_r1:frameworks/base/graphics/java/android/graphics/RecordingCanvas.java
+ */
+public final class RecordingCanvasNatives {
+
+  public static native long nCreateDisplayListCanvas(long node, int width, int height);
+
+  public static native void nResetDisplayListCanvas(long canvas, long node, int width, int height);
+
+  public static native int nGetMaximumTextureWidth();
+
+  public static native int nGetMaximumTextureHeight();
+
+  public static native void nEnableZ(long renderer, boolean enableZ);
+
+  public static native void nFinishRecording(long renderer, long renderNode);
+
+  public static native void nDrawRenderNode(long renderer, long renderNode);
+
+  public static native void nDrawTextureLayer(long renderer, long layer);
+
+  public static native void nDrawCircle(
+      long renderer, long propCx, long propCy, long propRadius, long propPaint);
+
+  public static native void nDrawRipple(
+      long renderer,
+      long propCx,
+      long propCy,
+      long propRadius,
+      long propPaint,
+      long propProgress,
+      long turbulencePhase,
+      int color,
+      long runtimeEffect);
+
+  public static native void nDrawRoundRect(
+      long renderer,
+      long propLeft,
+      long propTop,
+      long propRight,
+      long propBottom,
+      long propRx,
+      long propRy,
+      long propPaint);
+
+  public static native void nDrawWebViewFunctor(long canvas, int functor);
+
+  private RecordingCanvasNatives() {}
+}
diff --git a/nativeruntime/src/main/java/org/robolectric/nativeruntime/RegionIteratorNatives.java b/nativeruntime/src/main/java/org/robolectric/nativeruntime/RegionIteratorNatives.java
new file mode 100644
index 0000000..ea2f17d
--- /dev/null
+++ b/nativeruntime/src/main/java/org/robolectric/nativeruntime/RegionIteratorNatives.java
@@ -0,0 +1,20 @@
+package org.robolectric.nativeruntime;
+
+import android.graphics.Rect;
+
+/**
+ * Native methods for RegionIterator JNI registration.
+ *
+ * <p>Native method signatures are derived from
+ * https://cs.android.com/android/platform/superproject/+/android-12.0.0_r1:frameworks/base/graphics/java/android/graphics/RegionIterator.java
+ */
+public final class RegionIteratorNatives {
+
+  public static native long nativeConstructor(long nativeRegion);
+
+  public static native void nativeDestructor(long nativeIter);
+
+  public static native boolean nativeNext(long nativeIter, Rect r);
+
+  private RegionIteratorNatives() {}
+}
diff --git a/nativeruntime/src/main/java/org/robolectric/nativeruntime/RegionNatives.java b/nativeruntime/src/main/java/org/robolectric/nativeruntime/RegionNatives.java
new file mode 100644
index 0000000..c6d1bae
--- /dev/null
+++ b/nativeruntime/src/main/java/org/robolectric/nativeruntime/RegionNatives.java
@@ -0,0 +1,66 @@
+package org.robolectric.nativeruntime;
+
+import android.graphics.Rect;
+import android.graphics.Region;
+import android.os.Parcel;
+
+/**
+ * Native methods for Region JNI registration.
+ *
+ * <p>Native method signatures are derived from
+ * https://cs.android.com/android/platform/superproject/+/android-12.0.0_r1:frameworks/base/graphics/java/android/graphics/Region.java
+ */
+public final class RegionNatives {
+
+  // Must be this style to match AOSP branch
+  public long mNativeRegion;
+
+  public static native boolean nativeEquals(long nativeR1, long nativeR2);
+
+  public static native long nativeConstructor();
+
+  public static native void nativeDestructor(long nativeRegion);
+
+  public static native void nativeSetRegion(long nativeDst, long nativeSrc);
+
+  public static native boolean nativeSetRect(
+      long nativeDst, int left, int top, int right, int bottom);
+
+  public static native boolean nativeSetPath(long nativeDst, long nativePath, long nativeClip);
+
+  public static native boolean nativeGetBounds(long nativeRegion, Rect rect);
+
+  public static native boolean nativeGetBoundaryPath(long nativeRegion, long nativePath);
+
+  public static native boolean nativeOp(
+      long nativeDst, int left, int top, int right, int bottom, int op);
+
+  public static native boolean nativeOp(long nativeDst, Rect rect, long nativeRegion, int op);
+
+  public static native boolean nativeOp(
+      long nativeDst, long nativeRegion1, long nativeRegion2, int op);
+
+  public static native long nativeCreateFromParcel(Parcel p);
+
+  public static native boolean nativeWriteToParcel(long nativeRegion, Parcel p);
+
+  public static native String nativeToString(long nativeRegion);
+
+  public native boolean isEmpty();
+
+  public native boolean isRect();
+
+  public native boolean isComplex();
+
+  public native boolean contains(int x, int y);
+
+  public native boolean quickContains(int left, int top, int right, int bottom);
+
+  public native boolean quickReject(int left, int top, int right, int bottom);
+
+  public native boolean quickReject(Region rgn);
+
+  public native void translate(int dx, int dy, Region dst);
+
+  public native void scale(float scale, Region dst);
+}
diff --git a/nativeruntime/src/main/java/org/robolectric/nativeruntime/RenderEffectNatives.java b/nativeruntime/src/main/java/org/robolectric/nativeruntime/RenderEffectNatives.java
new file mode 100644
index 0000000..dcf82d1
--- /dev/null
+++ b/nativeruntime/src/main/java/org/robolectric/nativeruntime/RenderEffectNatives.java
@@ -0,0 +1,39 @@
+package org.robolectric.nativeruntime;
+
+/**
+ * Native methods for RenderEffect JNI registration.
+ *
+ * <p>Native method signatures are derived from
+ * https://cs.android.com/android/platform/superproject/+/android-12.0.0_r1:frameworks/base/graphics/java/android/graphics/RenderEffect.java
+ */
+public final class RenderEffectNatives {
+
+  public static native long nativeCreateOffsetEffect(
+      float offsetX, float offsetY, long nativeInput);
+
+  public static native long nativeCreateBlurEffect(
+      float radiusX, float radiusY, long nativeInput, int edgeTreatment);
+
+  public static native long nativeCreateBitmapEffect(
+      long bitmapHandle,
+      float srcLeft,
+      float srcTop,
+      float srcRight,
+      float srcBottom,
+      float dstLeft,
+      float dstTop,
+      float dstRight,
+      float dstBottom);
+
+  public static native long nativeCreateColorFilterEffect(long colorFilter, long nativeInput);
+
+  public static native long nativeCreateBlendModeEffect(long dst, long src, int blendmode);
+
+  public static native long nativeCreateChainEffect(long outer, long inner);
+
+  public static native long nativeCreateShaderEffect(long shader);
+
+  public static native long nativeGetFinalizer();
+
+  private RenderEffectNatives() {}
+}
diff --git a/nativeruntime/src/main/java/org/robolectric/nativeruntime/RenderNodeAnimatorNatives.java b/nativeruntime/src/main/java/org/robolectric/nativeruntime/RenderNodeAnimatorNatives.java
new file mode 100644
index 0000000..3d7de6e
--- /dev/null
+++ b/nativeruntime/src/main/java/org/robolectric/nativeruntime/RenderNodeAnimatorNatives.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.robolectric.nativeruntime;
+
+/**
+ * Native methods for RenderNodeAnimator JNI registration.
+ *
+ * <p>Native method signatures are derived from
+ * https://cs.android.com/android/platform/superproject/+/android-12.0.0_r1:frameworks/base/graphics/java/android/graphics/RenderNodeAnimator.java
+ */
+public final class RenderNodeAnimatorNatives {
+
+  public static native long nCreateAnimator(int property, float finalValue);
+
+  public static native long nCreateCanvasPropertyFloatAnimator(
+      long canvasProperty, float finalValue);
+
+  public static native long nCreateCanvasPropertyPaintAnimator(
+      long canvasProperty, int paintField, float finalValue);
+
+  public static native long nCreateRevealAnimator(int x, int y, float startRadius, float endRadius);
+
+  public static native void nSetStartValue(long nativePtr, float startValue);
+
+  public static native void nSetDuration(long nativePtr, long duration);
+
+  public static native long nGetDuration(long nativePtr);
+
+  public static native void nSetStartDelay(long nativePtr, long startDelay);
+
+  public static native void nSetInterpolator(long animPtr, long interpolatorPtr);
+
+  public static native void nSetAllowRunningAsync(long animPtr, boolean mayRunAsync);
+
+  public static native void nSetListener(long animPtr, Object listener);
+
+  public static native void nStart(long animPtr);
+
+  public static native void nEnd(long animPtr);
+
+  private RenderNodeAnimatorNatives() {}
+}
diff --git a/nativeruntime/src/main/java/org/robolectric/nativeruntime/RenderNodeNatives.java b/nativeruntime/src/main/java/org/robolectric/nativeruntime/RenderNodeNatives.java
new file mode 100644
index 0000000..adda69e
--- /dev/null
+++ b/nativeruntime/src/main/java/org/robolectric/nativeruntime/RenderNodeNatives.java
@@ -0,0 +1,211 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.robolectric.nativeruntime;
+
+import android.graphics.RenderNode.PositionUpdateListener;
+
+/**
+ * Native methods for RenderNode JNI registration.
+ *
+ * <p>Native method signatures are derived from
+ * https://cs.android.com/android/platform/superproject/+/android-12.0.0_r1:frameworks/base/graphics/java/android/graphics/RenderNode.java
+ */
+public final class RenderNodeNatives {
+
+  public static native long nCreate(String name);
+
+  public static native long nGetNativeFinalizer();
+
+  public static native void nOutput(long renderNode);
+
+  public static native int nGetUsageSize(long renderNode);
+
+  public static native int nGetAllocatedSize(long renderNode);
+
+  public static native void nRequestPositionUpdates(
+      long renderNode, PositionUpdateListener callback);
+
+  public static native void nAddAnimator(long renderNode, long animatorPtr);
+
+  public static native void nEndAllAnimators(long renderNode);
+
+  public static native void nDiscardDisplayList(long renderNode);
+
+  public static native boolean nIsValid(long renderNode);
+
+  public static native void nGetTransformMatrix(long renderNode, long nativeMatrix);
+
+  public static native void nGetInverseTransformMatrix(long renderNode, long nativeMatrix);
+
+  public static native boolean nHasIdentityMatrix(long renderNode);
+
+  public static native boolean nOffsetTopAndBottom(long renderNode, int offset);
+
+  public static native boolean nOffsetLeftAndRight(long renderNode, int offset);
+
+  public static native boolean nSetLeftTopRightBottom(
+      long renderNode, int left, int top, int right, int bottom);
+
+  public static native boolean nSetLeft(long renderNode, int left);
+
+  public static native boolean nSetTop(long renderNode, int top);
+
+  public static native boolean nSetRight(long renderNode, int right);
+
+  public static native boolean nSetBottom(long renderNode, int bottom);
+
+  public static native int nGetLeft(long renderNode);
+
+  public static native int nGetTop(long renderNode);
+
+  public static native int nGetRight(long renderNode);
+
+  public static native int nGetBottom(long renderNode);
+
+  public static native boolean nSetCameraDistance(long renderNode, float distance);
+
+  public static native boolean nSetPivotY(long renderNode, float pivotY);
+
+  public static native boolean nSetPivotX(long renderNode, float pivotX);
+
+  public static native boolean nResetPivot(long renderNode);
+
+  public static native boolean nSetLayerType(long renderNode, int layerType);
+
+  public static native int nGetLayerType(long renderNode);
+
+  public static native boolean nSetLayerPaint(long renderNode, long paint);
+
+  public static native boolean nSetClipToBounds(long renderNode, boolean clipToBounds);
+
+  public static native boolean nGetClipToBounds(long renderNode);
+
+  public static native boolean nSetClipBounds(
+      long renderNode, int left, int top, int right, int bottom);
+
+  public static native boolean nSetClipBoundsEmpty(long renderNode);
+
+  public static native boolean nSetProjectBackwards(long renderNode, boolean shouldProject);
+
+  public static native boolean nSetProjectionReceiver(long renderNode, boolean shouldReceive);
+
+  public static native boolean nSetOutlineRoundRect(
+      long renderNode, int left, int top, int right, int bottom, float radius, float alpha);
+
+  public static native boolean nSetOutlinePath(long renderNode, long nativePath, float alpha);
+
+  public static native boolean nSetOutlineEmpty(long renderNode);
+
+  public static native boolean nSetOutlineNone(long renderNode);
+
+  public static native boolean nClearStretch(long renderNode);
+
+  public static native boolean nStretch(
+      long renderNode, float vecX, float vecY, float maxStretchX, float maxStretchY);
+
+  public static native boolean nHasShadow(long renderNode);
+
+  public static native boolean nSetSpotShadowColor(long renderNode, int color);
+
+  public static native boolean nSetAmbientShadowColor(long renderNode, int color);
+
+  public static native int nGetSpotShadowColor(long renderNode);
+
+  public static native int nGetAmbientShadowColor(long renderNode);
+
+  public static native boolean nSetClipToOutline(long renderNode, boolean clipToOutline);
+
+  public static native boolean nSetRevealClip(
+      long renderNode, boolean shouldClip, float x, float y, float radius);
+
+  public static native boolean nSetAlpha(long renderNode, float alpha);
+
+  public static native boolean nSetRenderEffect(long renderNode, long renderEffect);
+
+  public static native boolean nSetHasOverlappingRendering(
+      long renderNode, boolean hasOverlappingRendering);
+
+  public static native void nSetUsageHint(long renderNode, int usageHint);
+
+  public static native boolean nSetElevation(long renderNode, float lift);
+
+  public static native boolean nSetTranslationX(long renderNode, float translationX);
+
+  public static native boolean nSetTranslationY(long renderNode, float translationY);
+
+  public static native boolean nSetTranslationZ(long renderNode, float translationZ);
+
+  public static native boolean nSetRotation(long renderNode, float rotation);
+
+  public static native boolean nSetRotationX(long renderNode, float rotationX);
+
+  public static native boolean nSetRotationY(long renderNode, float rotationY);
+
+  public static native boolean nSetScaleX(long renderNode, float scaleX);
+
+  public static native boolean nSetScaleY(long renderNode, float scaleY);
+
+  public static native boolean nSetStaticMatrix(long renderNode, long nativeMatrix);
+
+  public static native boolean nSetAnimationMatrix(long renderNode, long animationMatrix);
+
+  public static native boolean nHasOverlappingRendering(long renderNode);
+
+  public static native boolean nGetAnimationMatrix(long renderNode, long animationMatrix);
+
+  public static native boolean nGetClipToOutline(long renderNode);
+
+  public static native float nGetAlpha(long renderNode);
+
+  public static native float nGetCameraDistance(long renderNode);
+
+  public static native float nGetScaleX(long renderNode);
+
+  public static native float nGetScaleY(long renderNode);
+
+  public static native float nGetElevation(long renderNode);
+
+  public static native float nGetTranslationX(long renderNode);
+
+  public static native float nGetTranslationY(long renderNode);
+
+  public static native float nGetTranslationZ(long renderNode);
+
+  public static native float nGetRotation(long renderNode);
+
+  public static native float nGetRotationX(long renderNode);
+
+  public static native float nGetRotationY(long renderNode);
+
+  public static native boolean nIsPivotExplicitlySet(long renderNode);
+
+  public static native float nGetPivotX(long renderNode);
+
+  public static native float nGetPivotY(long renderNode);
+
+  public static native int nGetWidth(long renderNode);
+
+  public static native int nGetHeight(long renderNode);
+
+  public static native boolean nSetAllowForceDark(long renderNode, boolean allowForceDark);
+
+  public static native boolean nGetAllowForceDark(long renderNode);
+
+  public static native long nGetUniqueId(long renderNode);
+
+  private RenderNodeNatives() {}
+}
diff --git a/nativeruntime/src/main/java/org/robolectric/nativeruntime/RuntimeShaderNatives.java b/nativeruntime/src/main/java/org/robolectric/nativeruntime/RuntimeShaderNatives.java
new file mode 100644
index 0000000..6d4e49f
--- /dev/null
+++ b/nativeruntime/src/main/java/org/robolectric/nativeruntime/RuntimeShaderNatives.java
@@ -0,0 +1,23 @@
+package org.robolectric.nativeruntime;
+
+/**
+ * Native methods for RuntimeShader JNI registration.
+ *
+ * <p>Native method signatures are derived from
+ * https://cs.android.com/android/platform/superproject/+/android-12.0.0_r1:frameworks/base/graphics/java/android/graphics/RuntimeShader.java
+ */
+public class RuntimeShaderNatives {
+
+  public static native long nativeGetFinalizer();
+
+  public static native long nativeCreateBuilder(String sksl);
+
+  public static native long nativeCreateShader(long shaderBuilder, long matrix, boolean isOpaque);
+
+  public static native void nativeUpdateUniforms(
+      long shaderBuilder, String uniformName, float[] uniforms);
+
+  public static native void nativeUpdateShader(long shaderBuilder, String shaderName, long shader);
+
+  private RuntimeShaderNatives() {}
+}
diff --git a/nativeruntime/src/main/java/org/robolectric/nativeruntime/ShaderNatives.java b/nativeruntime/src/main/java/org/robolectric/nativeruntime/ShaderNatives.java
new file mode 100644
index 0000000..b50fa5f
--- /dev/null
+++ b/nativeruntime/src/main/java/org/robolectric/nativeruntime/ShaderNatives.java
@@ -0,0 +1,14 @@
+package org.robolectric.nativeruntime;
+
+/**
+ * Native methods for Shader JNI registration.
+ *
+ * <p>Native method signatures are derived from
+ * https://cs.android.com/android/platform/superproject/+/android-12.0.0_r1:frameworks/base/graphics/java/android/graphics/Shader.java
+ */
+public final class ShaderNatives {
+
+  public static native long nativeGetFinalizer();
+
+  private ShaderNatives() {}
+}
diff --git a/nativeruntime/src/main/java/org/robolectric/nativeruntime/SumPathEffectNatives.java b/nativeruntime/src/main/java/org/robolectric/nativeruntime/SumPathEffectNatives.java
new file mode 100644
index 0000000..d7edf0e
--- /dev/null
+++ b/nativeruntime/src/main/java/org/robolectric/nativeruntime/SumPathEffectNatives.java
@@ -0,0 +1,14 @@
+package org.robolectric.nativeruntime;
+
+/**
+ * Native methods for SumPathEffect JNI registration.
+ *
+ * <p>Native method signatures are derived from
+ * https://cs.android.com/android/platform/superproject/+/android-12.0.0_r1:frameworks/base/graphics/java/android/graphics/SumPathEffect.java
+ */
+public final class SumPathEffectNatives {
+
+  public static native long nativeCreate(long first, long second);
+
+  private SumPathEffectNatives() {}
+}
diff --git a/nativeruntime/src/main/java/org/robolectric/nativeruntime/SurfaceNatives.java b/nativeruntime/src/main/java/org/robolectric/nativeruntime/SurfaceNatives.java
new file mode 100644
index 0000000..882d811
--- /dev/null
+++ b/nativeruntime/src/main/java/org/robolectric/nativeruntime/SurfaceNatives.java
@@ -0,0 +1,80 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.robolectric.nativeruntime;
+
+import android.graphics.Canvas;
+import android.graphics.Rect;
+import android.graphics.SurfaceTexture;
+import android.hardware.HardwareBuffer;
+import android.os.Parcel;
+
+/**
+ * Native methods for Surface JNI registration.
+ *
+ * <p>Native method signatures are derived from
+ * https://cs.android.com/android/platform/superproject/+/android-12.0.0_r1:frameworks/base/graphics/java/android/view/Surface.java
+ */
+public final class SurfaceNatives {
+
+  public static native long nativeCreateFromSurfaceTexture(SurfaceTexture surfaceTexture);
+
+  public static native long nativeCreateFromSurfaceControl(long surfaceControlNativeObject);
+
+  public static native long nativeGetFromSurfaceControl(
+      long surfaceObject, long surfaceControlNativeObject);
+
+  public static native long nativeGetFromBlastBufferQueue(
+      long surfaceObject, long blastBufferQueueNativeObject);
+
+  public static native long nativeLockCanvas(long nativeObject, Canvas canvas, Rect dirty);
+
+  public static native void nativeUnlockCanvasAndPost(long nativeObject, Canvas canvas);
+
+  public static native void nativeRelease(long nativeObject);
+
+  public static native boolean nativeIsValid(long nativeObject);
+
+  public static native boolean nativeIsConsumerRunningBehind(long nativeObject);
+
+  public static native long nativeReadFromParcel(long nativeObject, Parcel source);
+
+  public static native void nativeWriteToParcel(long nativeObject, Parcel dest);
+
+  public static native void nativeAllocateBuffers(long nativeObject);
+
+  public static native int nativeGetWidth(long nativeObject);
+
+  public static native int nativeGetHeight(long nativeObject);
+
+  public static native long nativeGetNextFrameNumber(long nativeObject);
+
+  public static native int nativeSetScalingMode(long nativeObject, int scalingMode);
+
+  public static native int nativeForceScopedDisconnect(long nativeObject);
+
+  public static native int nativeAttachAndQueueBufferWithColorSpace(
+      long nativeObject, HardwareBuffer buffer, int colorSpaceId);
+
+  public static native int nativeSetSharedBufferModeEnabled(long nativeObject, boolean enabled);
+
+  public static native int nativeSetAutoRefreshEnabled(long nativeObject, boolean enabled);
+
+  public static native int nativeSetFrameRate(
+      long nativeObject, float frameRate, int compatibility, int changeFrameRateStrategy);
+
+  private SurfaceNatives() {}
+}
diff --git a/nativeruntime/src/main/java/org/robolectric/nativeruntime/SweepGradientNatives.java b/nativeruntime/src/main/java/org/robolectric/nativeruntime/SweepGradientNatives.java
new file mode 100644
index 0000000..85d5a2d
--- /dev/null
+++ b/nativeruntime/src/main/java/org/robolectric/nativeruntime/SweepGradientNatives.java
@@ -0,0 +1,20 @@
+package org.robolectric.nativeruntime;
+
+/**
+ * Native methods for SweepGradient JNI registration.
+ *
+ * <p>Native method signatures are derived from
+ * https://cs.android.com/android/platform/superproject/+/android-12.0.0_r1:frameworks/base/graphics/java/android/graphics/SweepGradient.java
+ */
+public class SweepGradientNatives {
+
+  public static native long nativeCreate(
+      long matrix, float x, float y, long[] colors, float[] positions, long colorSpaceHandle);
+
+  public static native long nativeCreate1(
+      long matrix, float x, float y, int[] colors, float[] positions);
+
+  public static native long nativeCreate2(long matrix, float x, float y, int color0, int color1);
+
+  private SweepGradientNatives() {}
+}
diff --git a/nativeruntime/src/main/java/org/robolectric/nativeruntime/TableMaskFilterNatives.java b/nativeruntime/src/main/java/org/robolectric/nativeruntime/TableMaskFilterNatives.java
new file mode 100644
index 0000000..ca7f4f0
--- /dev/null
+++ b/nativeruntime/src/main/java/org/robolectric/nativeruntime/TableMaskFilterNatives.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2006 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.robolectric.nativeruntime;
+
+/**
+ * Native methods for TableMaskFilter JNI registration.
+ *
+ * <p>Native method signatures are derived from
+ * https://cs.android.com/android/platform/superproject/+/android-12.0.0_r1:frameworks/base/graphics/java/android/graphics/TableMaskFilter.java
+ */
+public final class TableMaskFilterNatives {
+
+  public static native long nativeNewTable(byte[] table);
+
+  public static native long nativeNewClip(int min, int max);
+
+  public static native long nativeNewGamma(float gamma);
+
+  private TableMaskFilterNatives() {}
+}
diff --git a/nativeruntime/src/main/java/org/robolectric/nativeruntime/TypefaceNatives.java b/nativeruntime/src/main/java/org/robolectric/nativeruntime/TypefaceNatives.java
new file mode 100644
index 0000000..204d89a
--- /dev/null
+++ b/nativeruntime/src/main/java/org/robolectric/nativeruntime/TypefaceNatives.java
@@ -0,0 +1,72 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.robolectric.nativeruntime;
+
+import android.graphics.Typeface;
+import android.graphics.fonts.FontVariationAxis;
+import java.nio.ByteBuffer;
+import java.util.List;
+
+/**
+ * Native methods for Typeface JNI registration.
+ *
+ * <p>Native method signatures are derived from
+ * https://cs.android.com/android/platform/superproject/+/android-12.0.0_r1:frameworks/base/graphics/java/android/graphics/Typeface.java
+ */
+public final class TypefaceNatives {
+
+  public static native long nativeCreateFromTypeface(long nativeInstance, int style);
+
+  public static native long nativeCreateFromTypefaceWithExactStyle(
+      long nativeInstance, int weight, boolean italic);
+
+  public static native long nativeCreateFromTypefaceWithVariation(
+      long nativeInstance, List<FontVariationAxis> axes);
+
+  public static native long nativeCreateWeightAlias(long nativeInstance, int weight);
+
+  public static native long nativeCreateFromArray(
+      long[] familyArray, long fallbackTypeface, int weight, int italic);
+
+  public static native int[] nativeGetSupportedAxes(long nativeInstance);
+
+  public static native void nativeSetDefault(long nativePtr);
+
+  public static native int nativeGetStyle(long nativePtr);
+
+  public static native int nativeGetWeight(long nativePtr);
+
+  public static native long nativeGetReleaseFunc();
+
+  public static native int nativeGetFamilySize(long naitvePtr);
+
+  public static native long nativeGetFamily(long nativePtr, int index);
+
+  public static native void nativeRegisterGenericFamily(String str, long nativePtr);
+
+  public static native int nativeWriteTypefaces(ByteBuffer buffer, long[] nativePtrs);
+
+  public static native long[] nativeReadTypefaces(ByteBuffer buffer);
+
+  public static native void nativeForceSetStaticFinalField(String fieldName, Typeface typeface);
+
+  public static native void nativeAddFontCollections(long nativePtr);
+
+  public static native void nativeWarmUpCache(String fileName);
+
+  private TypefaceNatives() {}
+}
diff --git a/nativeruntime/src/main/java/org/robolectric/nativeruntime/VectorDrawableNatives.java b/nativeruntime/src/main/java/org/robolectric/nativeruntime/VectorDrawableNatives.java
new file mode 100644
index 0000000..39c0542
--- /dev/null
+++ b/nativeruntime/src/main/java/org/robolectric/nativeruntime/VectorDrawableNatives.java
@@ -0,0 +1,150 @@
+package org.robolectric.nativeruntime;
+
+import android.graphics.Rect;
+
+/**
+ * Native methods for VectorDrawable JNI registration.
+ *
+ * <p>Native method signatures are derived from
+ * https://cs.android.com/android/platform/superproject/+/android-12.0.0_r1:frameworks/base/graphics/java/android/graphics/VectorDrawable.java
+ */
+public final class VectorDrawableNatives {
+
+  public static native int nDraw(
+      long rendererPtr,
+      long canvasWrapperPtr,
+      long colorFilterPtr,
+      Rect bounds,
+      boolean needsMirroring,
+      boolean canReuseCache);
+
+  public static native boolean nGetFullPathProperties(long pathPtr, byte[] properties, int length);
+
+  public static native void nSetName(long nodePtr, String name);
+
+  public static native boolean nGetGroupProperties(long groupPtr, float[] properties, int length);
+
+  public static native void nSetPathString(long pathPtr, String pathString, int length);
+
+  public static native long nCreateTree(long rootGroupPtr);
+
+  public static native long nCreateTreeFromCopy(long treeToCopy, long rootGroupPtr);
+
+  public static native void nSetRendererViewportSize(
+      long rendererPtr, float viewportWidth, float viewportHeight);
+
+  public static native boolean nSetRootAlpha(long rendererPtr, float alpha);
+
+  public static native float nGetRootAlpha(long rendererPtr);
+
+  public static native void nSetAntiAlias(long rendererPtr, boolean aa);
+
+  public static native void nSetAllowCaching(long rendererPtr, boolean allowCaching);
+
+  public static native long nCreateFullPath();
+
+  public static native long nCreateFullPath(long nativeFullPathPtr);
+
+  public static native void nUpdateFullPathProperties(
+      long pathPtr,
+      float strokeWidth,
+      int strokeColor,
+      float strokeAlpha,
+      int fillColor,
+      float fillAlpha,
+      float trimPathStart,
+      float trimPathEnd,
+      float trimPathOffset,
+      float strokeMiterLimit,
+      int strokeLineCap,
+      int strokeLineJoin,
+      int fillType);
+
+  public static native void nUpdateFullPathFillGradient(long pathPtr, long fillGradientPtr);
+
+  public static native void nUpdateFullPathStrokeGradient(long pathPtr, long strokeGradientPtr);
+
+  public static native long nCreateClipPath();
+
+  public static native long nCreateClipPath(long clipPathPtr);
+
+  public static native long nCreateGroup();
+
+  public static native long nCreateGroup(long groupPtr);
+
+  public static native void nUpdateGroupProperties(
+      long groupPtr,
+      float rotate,
+      float pivotX,
+      float pivotY,
+      float scaleX,
+      float scaleY,
+      float translateX,
+      float translateY);
+
+  public static native void nAddChild(long groupPtr, long nodePtr);
+
+  public static native float nGetRotation(long groupPtr);
+
+  public static native void nSetRotation(long groupPtr, float rotation);
+
+  public static native float nGetPivotX(long groupPtr);
+
+  public static native void nSetPivotX(long groupPtr, float pivotX);
+
+  public static native float nGetPivotY(long groupPtr);
+
+  public static native void nSetPivotY(long groupPtr, float pivotY);
+
+  public static native float nGetScaleX(long groupPtr);
+
+  public static native void nSetScaleX(long groupPtr, float scaleX);
+
+  public static native float nGetScaleY(long groupPtr);
+
+  public static native void nSetScaleY(long groupPtr, float scaleY);
+
+  public static native float nGetTranslateX(long groupPtr);
+
+  public static native void nSetTranslateX(long groupPtr, float translateX);
+
+  public static native float nGetTranslateY(long groupPtr);
+
+  public static native void nSetTranslateY(long groupPtr, float translateY);
+
+  public static native void nSetPathData(long pathPtr, long pathDataPtr);
+
+  public static native float nGetStrokeWidth(long pathPtr);
+
+  public static native void nSetStrokeWidth(long pathPtr, float width);
+
+  public static native int nGetStrokeColor(long pathPtr);
+
+  public static native void nSetStrokeColor(long pathPtr, int strokeColor);
+
+  public static native float nGetStrokeAlpha(long pathPtr);
+
+  public static native void nSetStrokeAlpha(long pathPtr, float alpha);
+
+  public static native int nGetFillColor(long pathPtr);
+
+  public static native void nSetFillColor(long pathPtr, int fillColor);
+
+  public static native float nGetFillAlpha(long pathPtr);
+
+  public static native void nSetFillAlpha(long pathPtr, float fillAlpha);
+
+  public static native float nGetTrimPathStart(long pathPtr);
+
+  public static native void nSetTrimPathStart(long pathPtr, float trimPathStart);
+
+  public static native float nGetTrimPathEnd(long pathPtr);
+
+  public static native void nSetTrimPathEnd(long pathPtr, float trimPathEnd);
+
+  public static native float nGetTrimPathOffset(long pathPtr);
+
+  public static native void nSetTrimPathOffset(long pathPtr, float trimPathOffset);
+
+  private VectorDrawableNatives() {}
+}
diff --git a/nativeruntime/src/main/java/org/robolectric/nativeruntime/VirtualRefBasePtrNatives.java b/nativeruntime/src/main/java/org/robolectric/nativeruntime/VirtualRefBasePtrNatives.java
new file mode 100644
index 0000000..0c96f08
--- /dev/null
+++ b/nativeruntime/src/main/java/org/robolectric/nativeruntime/VirtualRefBasePtrNatives.java
@@ -0,0 +1,16 @@
+package org.robolectric.nativeruntime;
+
+/**
+ * Native methods for VirtualRefBasePtr JNI registration.
+ *
+ * <p>Native method signatures are derived from
+ * https://cs.android.com/android/platform/superproject/+/android-12.0.0_r1:frameworks/base/graphics/java/android/graphics/VirtualRefBasePtr.java
+ */
+public final class VirtualRefBasePtrNatives {
+
+  public static native void nIncStrong(long ptr);
+
+  public static native void nDecStrong(long ptr);
+
+  private VirtualRefBasePtrNatives() {}
+}
diff --git a/nativeruntime/src/test/java/org/robolectric/nativeruntime/DefaultNativeRuntimeLazyLoadTest.java b/nativeruntime/src/test/java/org/robolectric/nativeruntime/DefaultNativeRuntimeLazyLoadTest.java
new file mode 100644
index 0000000..ec86818
--- /dev/null
+++ b/nativeruntime/src/test/java/org/robolectric/nativeruntime/DefaultNativeRuntimeLazyLoadTest.java
@@ -0,0 +1,29 @@
+package org.robolectric.nativeruntime;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.app.Application;
+import android.database.CursorWindow;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.RuntimeEnvironment;
+
+@RunWith(RobolectricTestRunner.class)
+public final class DefaultNativeRuntimeLazyLoadTest {
+
+  /**
+   * Checks to see that RNR is not loaded by default when an empty application is created. RNR load
+   * times are typically 0.5-1s, so it is desirable to have it lazy loaded when native code is
+   * called.
+   */
+  @SuppressWarnings("UnusedVariable")
+  @Test
+  public void lazyLoad() throws Exception {
+    Application application = RuntimeEnvironment.getApplication();
+    assertThat(DefaultNativeRuntimeLoader.isLoaded()).isFalse();
+    CursorWindow cursorWindow = new CursorWindow("hi");
+    cursorWindow.close();
+    assertThat(DefaultNativeRuntimeLoader.isLoaded()).isTrue();
+  }
+}
diff --git a/nativeruntime/src/test/java/org/robolectric/nativeruntime/DefaultNativeRuntimeLoaderTest.java b/nativeruntime/src/test/java/org/robolectric/nativeruntime/DefaultNativeRuntimeLoaderTest.java
new file mode 100644
index 0000000..cbb9cf1
--- /dev/null
+++ b/nativeruntime/src/test/java/org/robolectric/nativeruntime/DefaultNativeRuntimeLoaderTest.java
@@ -0,0 +1,21 @@
+package org.robolectric.nativeruntime;
+
+import android.database.CursorWindow;
+import android.database.sqlite.SQLiteDatabase;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+
+@RunWith(RobolectricTestRunner.class)
+public final class DefaultNativeRuntimeLoaderTest {
+  ExecutorService executor = Executors.newSingleThreadExecutor();
+
+  @Test
+  public void concurrentLoad() throws Exception {
+    executor.execute(() -> SQLiteDatabase.create(null));
+    CursorWindow cursorWindow = new CursorWindow("sdfsdf");
+    cursorWindow.close();
+  }
+}
diff --git a/plugins/maven-dependency-resolver/src/main/java/org/robolectric/MavenRoboSettings.java b/plugins/maven-dependency-resolver/src/main/java/org/robolectric/MavenRoboSettings.java
index 527ee33..0c8d069 100644
--- a/plugins/maven-dependency-resolver/src/main/java/org/robolectric/MavenRoboSettings.java
+++ b/plugins/maven-dependency-resolver/src/main/java/org/robolectric/MavenRoboSettings.java
@@ -7,11 +7,13 @@
  */
 @Deprecated
 public class MavenRoboSettings {
-
+  private static final int DEFAULT_PROXY_PORT = 0;
   private static String mavenRepositoryId;
   private static String mavenRepositoryUrl;
   private static String mavenRepositoryUserName;
   private static String mavenRepositoryPassword;
+  private static String mavenProxyHost = "";
+  private static int mavenProxyPort = DEFAULT_PROXY_PORT;
 
   static {
     mavenRepositoryId = System.getProperty("robolectric.dependency.repo.id", "mavenCentral");
@@ -19,6 +21,20 @@
         System.getProperty("robolectric.dependency.repo.url", "https://repo1.maven.org/maven2");
     mavenRepositoryUserName = System.getProperty("robolectric.dependency.repo.username");
     mavenRepositoryPassword = System.getProperty("robolectric.dependency.repo.password");
+
+    String proxyHost = System.getProperty("robolectric.dependency.proxy.host");
+    if (proxyHost != null && !proxyHost.isEmpty()) {
+      mavenProxyHost = proxyHost;
+    }
+
+    String proxyPort = System.getProperty("robolectric.dependency.proxy.port");
+    if (proxyPort != null && !proxyPort.isEmpty()) {
+      try {
+        mavenProxyPort = Integer.parseInt(proxyPort);
+      } catch (NumberFormatException numberFormatException) {
+        mavenProxyPort = DEFAULT_PROXY_PORT;
+      }
+    }
   }
 
   public static String getMavenRepositoryId() {
@@ -52,4 +68,20 @@
   public static void setMavenRepositoryPassword(String mavenRepositoryPassword) {
     MavenRoboSettings.mavenRepositoryPassword = mavenRepositoryPassword;
   }
+
+  public static String getMavenProxyHost() {
+    return mavenProxyHost;
+  }
+
+  public static void setMavenProxyHost(String mavenProxyHost) {
+    MavenRoboSettings.mavenProxyHost = mavenProxyHost;
+  }
+
+  public static int getMavenProxyPort() {
+    return mavenProxyPort;
+  }
+
+  public static void setMavenProxyPort(int mavenProxyPort) {
+    MavenRoboSettings.mavenProxyPort = mavenProxyPort;
+  }
 }
diff --git a/plugins/maven-dependency-resolver/src/main/java/org/robolectric/internal/dependency/MavenArtifactFetcher.java b/plugins/maven-dependency-resolver/src/main/java/org/robolectric/internal/dependency/MavenArtifactFetcher.java
index 9b3a28a..60f852d 100644
--- a/plugins/maven-dependency-resolver/src/main/java/org/robolectric/internal/dependency/MavenArtifactFetcher.java
+++ b/plugins/maven-dependency-resolver/src/main/java/org/robolectric/internal/dependency/MavenArtifactFetcher.java
@@ -14,7 +14,9 @@
 import java.io.FileOutputStream;
 import java.io.IOException;
 import java.io.InputStream;
+import java.net.InetSocketAddress;
 import java.net.MalformedURLException;
+import java.net.Proxy;
 import java.net.URI;
 import java.net.URISyntaxException;
 import java.net.URL;
@@ -34,6 +36,8 @@
   private final String repositoryUrl;
   private final String repositoryUserName;
   private final String repositoryPassword;
+  private final String proxyHost;
+  private final int proxyPort;
   private final File localRepositoryDir;
   private final ExecutorService executorService;
   private File stagingRepositoryDir;
@@ -42,11 +46,15 @@
       String repositoryUrl,
       String repositoryUserName,
       String repositoryPassword,
+      String proxyHost,
+      int proxyPort,
       File localRepositoryDir,
       ExecutorService executorService) {
     this.repositoryUrl = repositoryUrl;
     this.repositoryUserName = repositoryUserName;
     this.repositoryPassword = repositoryPassword;
+    this.proxyHost = proxyHost;
+    this.proxyPort = proxyPort;
     this.localRepositoryDir = localRepositoryDir;
     this.executorService = executorService;
   }
@@ -152,7 +160,8 @@
 
   protected ListenableFuture<Void> createFetchToFileTask(URL remoteUrl, File tempFile) {
     return Futures.submitAsync(
-        new FetchToFileTask(remoteUrl, tempFile, repositoryUserName, repositoryPassword),
+        new FetchToFileTask(
+            remoteUrl, tempFile, repositoryUserName, repositoryPassword, proxyHost, proxyPort),
         this.executorService);
   }
 
@@ -168,18 +177,34 @@
     private final File localFile;
     private String repositoryUserName;
     private String repositoryPassword;
+    private String proxyHost;
+    private int proxyPort;
 
     public FetchToFileTask(
-        URL remoteURL, File localFile, String repositoryUserName, String repositoryPassword) {
+        URL remoteURL,
+        File localFile,
+        String repositoryUserName,
+        String repositoryPassword,
+        String proxyHost,
+        int proxyPort) {
       this.remoteURL = remoteURL;
       this.localFile = localFile;
       this.repositoryUserName = repositoryUserName;
       this.repositoryPassword = repositoryPassword;
+      this.proxyHost = proxyHost;
+      this.proxyPort = proxyPort;
     }
 
     @Override
     public ListenableFuture<Void> call() throws Exception {
-      URLConnection connection = remoteURL.openConnection();
+      URLConnection connection;
+      if (this.proxyHost != null && !this.proxyHost.isEmpty() && this.proxyPort > 0) {
+        Proxy proxy =
+            new Proxy(Proxy.Type.HTTP, new InetSocketAddress(this.proxyHost, this.proxyPort));
+        connection = remoteURL.openConnection(proxy);
+      } else {
+        connection = remoteURL.openConnection();
+      }
       // Add authorization header if applicable.
       if (!Strings.isNullOrEmpty(this.repositoryUserName)) {
         String encoded =
diff --git a/plugins/maven-dependency-resolver/src/main/java/org/robolectric/internal/dependency/MavenDependencyResolver.java b/plugins/maven-dependency-resolver/src/main/java/org/robolectric/internal/dependency/MavenDependencyResolver.java
index 22adfae..bb5604d 100755
--- a/plugins/maven-dependency-resolver/src/main/java/org/robolectric/internal/dependency/MavenDependencyResolver.java
+++ b/plugins/maven-dependency-resolver/src/main/java/org/robolectric/internal/dependency/MavenDependencyResolver.java
@@ -44,11 +44,22 @@
   private final File localRepositoryDir;
 
   public MavenDependencyResolver() {
-    this(MavenRoboSettings.getMavenRepositoryUrl(), MavenRoboSettings.getMavenRepositoryId(), MavenRoboSettings
-        .getMavenRepositoryUserName(), MavenRoboSettings.getMavenRepositoryPassword());
+    this(
+        MavenRoboSettings.getMavenRepositoryUrl(),
+        MavenRoboSettings.getMavenRepositoryId(),
+        MavenRoboSettings.getMavenRepositoryUserName(),
+        MavenRoboSettings.getMavenRepositoryPassword(),
+        MavenRoboSettings.getMavenProxyHost(),
+        MavenRoboSettings.getMavenProxyPort());
   }
 
-  public MavenDependencyResolver(String repositoryUrl, String repositoryId, String repositoryUserName, String repositoryPassword) {
+  public MavenDependencyResolver(
+      String repositoryUrl,
+      String repositoryId,
+      String repositoryUserName,
+      String repositoryPassword,
+      String proxyHost,
+      int proxyPort) {
     this.executorService = createExecutorService();
     this.localRepositoryDir = getLocalRepositoryDir();
     this.mavenArtifactFetcher =
@@ -56,6 +67,8 @@
             repositoryUrl,
             repositoryUserName,
             repositoryPassword,
+            proxyHost,
+            proxyPort,
             localRepositoryDir,
             this.executorService);
   }
@@ -163,10 +176,18 @@
       String repositoryUrl,
       String repositoryUserName,
       String repositoryPassword,
+      String proxyHost,
+      int proxyPort,
       File localRepositoryDir,
       ExecutorService executorService) {
     return new MavenArtifactFetcher(
-        repositoryUrl, repositoryUserName, repositoryPassword, localRepositoryDir, executorService);
+        repositoryUrl,
+        repositoryUserName,
+        repositoryPassword,
+        proxyHost,
+        proxyPort,
+        localRepositoryDir,
+        executorService);
   }
 
   protected ExecutorService createExecutorService() {
diff --git a/plugins/maven-dependency-resolver/src/test/java/org/robolectric/MavenRoboSettingsTest.java b/plugins/maven-dependency-resolver/src/test/java/org/robolectric/MavenRoboSettingsTest.java
index 164203b..8924257 100644
--- a/plugins/maven-dependency-resolver/src/test/java/org/robolectric/MavenRoboSettingsTest.java
+++ b/plugins/maven-dependency-resolver/src/test/java/org/robolectric/MavenRoboSettingsTest.java
@@ -15,6 +15,8 @@
   private String originalMavenRepositoryUrl;
   private String originalMavenRepositoryUserName;
   private String originalMavenRepositoryPassword;
+  private String originalMavenRepositoryProxyHost;
+  private int originalMavenProxyPort;
 
   @Before
   public void setUp() {
@@ -22,6 +24,8 @@
     originalMavenRepositoryUrl = MavenRoboSettings.getMavenRepositoryUrl();
     originalMavenRepositoryUserName = MavenRoboSettings.getMavenRepositoryUserName();
     originalMavenRepositoryPassword = MavenRoboSettings.getMavenRepositoryPassword();
+    originalMavenRepositoryProxyHost = MavenRoboSettings.getMavenProxyHost();
+    originalMavenProxyPort = MavenRoboSettings.getMavenProxyPort();
   }
 
   @After
@@ -30,6 +34,8 @@
     MavenRoboSettings.setMavenRepositoryUrl(originalMavenRepositoryUrl);
     MavenRoboSettings.setMavenRepositoryUserName(originalMavenRepositoryUserName);
     MavenRoboSettings.setMavenRepositoryPassword(originalMavenRepositoryPassword);
+    MavenRoboSettings.setMavenProxyHost(originalMavenRepositoryProxyHost);
+    MavenRoboSettings.setMavenProxyPort(originalMavenProxyPort);
   }
 
   @Test
@@ -65,4 +71,16 @@
     MavenRoboSettings.setMavenRepositoryPassword("password");
     assertEquals("password", MavenRoboSettings.getMavenRepositoryPassword());
   }
+
+  @Test
+  public void setMavenProxyHost() {
+    MavenRoboSettings.setMavenProxyHost("123.4.5.678");
+    assertEquals("123.4.5.678", MavenRoboSettings.getMavenProxyHost());
+  }
+
+  @Test
+  public void setMavenProxyPort() {
+    MavenRoboSettings.setMavenProxyPort(9000);
+    assertEquals(9000, MavenRoboSettings.getMavenProxyPort());
+  }
 }
diff --git a/plugins/maven-dependency-resolver/src/test/java/org/robolectric/internal/dependency/MavenDependencyResolverTest.java b/plugins/maven-dependency-resolver/src/test/java/org/robolectric/internal/dependency/MavenDependencyResolverTest.java
index 3849c03..f438414 100644
--- a/plugins/maven-dependency-resolver/src/test/java/org/robolectric/internal/dependency/MavenDependencyResolverTest.java
+++ b/plugins/maven-dependency-resolver/src/test/java/org/robolectric/internal/dependency/MavenDependencyResolverTest.java
@@ -27,6 +27,8 @@
   private static final String REPOSITORY_URL;
   private static final String REPOSITORY_USERNAME = "username";
   private static final String REPOSITORY_PASSWORD = "password";
+  private static final String PROXY_HOST = "123.4.5.678";
+  private static final int PROXY_PORT = 9000;
   private static final HashFunction SHA512 = Hashing.sha512();
 
   private static DependencyJar[] successCases =
@@ -65,6 +67,8 @@
             REPOSITORY_URL,
             REPOSITORY_USERNAME,
             REPOSITORY_PASSWORD,
+            PROXY_HOST,
+            PROXY_PORT,
             localRepositoryDir,
             executorService);
     mavenDependencyResolver = new TestMavenDependencyResolver();
@@ -167,6 +171,8 @@
         String repositoryUrl,
         String repositoryUserName,
         String repositoryPassword,
+        String proxyHost,
+        int proxyPort,
         File localRepositoryDir,
         ExecutorService executorService) {
       return mavenArtifactFetcher;
@@ -200,12 +206,16 @@
         String repositoryUrl,
         String repositoryUserName,
         String repositoryPassword,
+        String proxyHost,
+        int proxyPort,
         File localRepositoryDir,
         ExecutorService executorService) {
       super(
           repositoryUrl,
           repositoryUserName,
           repositoryPassword,
+          proxyHost,
+          proxyPort,
           localRepositoryDir,
           executorService);
       this.executorService = executorService;
@@ -214,7 +224,7 @@
     @Override
     protected ListenableFuture<Void> createFetchToFileTask(URL remoteUrl, File tempFile) {
       return Futures.submitAsync(
-          new FetchToFileTask(remoteUrl, tempFile, null, null) {
+          new FetchToFileTask(remoteUrl, tempFile, null, null, null, 0) {
             @Override
             public ListenableFuture<Void> call() throws Exception {
               numRequests += 1;
diff --git a/preinstrumented/src/main/java/org/robolectric/preinstrumented/JarInstrumentor.java b/preinstrumented/src/main/java/org/robolectric/preinstrumented/JarInstrumentor.java
index ed57692..a9c5ace 100644
--- a/preinstrumented/src/main/java/org/robolectric/preinstrumented/JarInstrumentor.java
+++ b/preinstrumented/src/main/java/org/robolectric/preinstrumented/JarInstrumentor.java
@@ -8,6 +8,7 @@
 import java.io.InputStream;
 import java.util.Enumeration;
 import java.util.Locale;
+import java.util.Properties;
 import java.util.jar.JarEntry;
 import java.util.jar.JarFile;
 import java.util.jar.JarOutputStream;
@@ -64,6 +65,13 @@
     int nonClassCount = 0;
     int classCount = 0;
 
+    // get the jar's SDK version
+    try {
+      classInstrumentor.setAndroidJarSDKVersion(getJarAndroidSDKVersion(jarFile));
+    } catch (Exception e) {
+      throw new AssertionError("Unable to get Android SDK version from Jar File", e);
+    }
+
     try (JarOutputStream jarOut =
         new JarOutputStream(new BufferedOutputStream(new FileOutputStream(destFile), ONE_MB))) {
       Enumeration<JarEntry> entries = jarFile.entries();
@@ -136,4 +144,16 @@
     entry.setTime(original.getTime());
     return entry;
   }
+
+  private int getJarAndroidSDKVersion(JarFile jarFile) throws IOException {
+    ZipEntry buildProp = jarFile.getEntry("build.prop");
+    Properties buildProps = new Properties();
+    buildProps.load(jarFile.getInputStream(buildProp));
+    String codename = buildProps.getProperty("ro.build.version.codename");
+    // Check for a prerelease SDK.
+    if (!"REL".equals(codename)) {
+      return 10000;
+    }
+    return Integer.parseInt(buildProps.getProperty("ro.build.version.sdk"));
+  }
 }
diff --git a/processor/src/main/java/org/robolectric/annotation/processing/RobolectricModel.java b/processor/src/main/java/org/robolectric/annotation/processing/RobolectricModel.java
index 79c2461..fd5e77d 100644
--- a/processor/src/main/java/org/robolectric/annotation/processing/RobolectricModel.java
+++ b/processor/src/main/java/org/robolectric/annotation/processing/RobolectricModel.java
@@ -4,6 +4,7 @@
 import static com.google.common.collect.Maps.newTreeMap;
 import static com.google.common.collect.Sets.newTreeSet;
 
+import com.google.auto.common.MoreTypes;
 import com.google.common.collect.HashMultimap;
 import com.google.common.collect.Iterables;
 import com.google.common.collect.Multimaps;
@@ -99,10 +100,9 @@
       TypeElement shadowBaseType = null;
       if (shadowPickerType != null) {
         TypeMirror iface = helpers.findInterface(shadowPickerType, ShadowPicker.class);
-        if (iface != null) {
-          com.sun.tools.javac.code.Type type = ((com.sun.tools.javac.code.Type.ClassType) iface)
-              .allparams().get(0);
-          String baseClassName = type.asElement().getQualifiedName().toString();
+        if (iface instanceof DeclaredType) {
+          TypeMirror first = MoreTypes.asDeclared(iface).getTypeArguments().get(0);
+          String baseClassName = first.toString();
           shadowBaseType = helpers.getTypeElement(baseClassName);
         }
       }
diff --git a/resources/src/main/java/org/robolectric/res/android/FileMap.java b/resources/src/main/java/org/robolectric/res/android/FileMap.java
index f127268..0672bbd 100644
--- a/resources/src/main/java/org/robolectric/res/android/FileMap.java
+++ b/resources/src/main/java/org/robolectric/res/android/FileMap.java
@@ -7,6 +7,7 @@
 
 import com.google.common.collect.ImmutableMap;
 import com.google.common.primitives.Ints;
+import com.google.common.primitives.Longs;
 import com.google.common.primitives.Shorts;
 import java.io.File;
 import java.io.FileInputStream;
@@ -23,11 +24,20 @@
   /** ZIP archive central directory end header signature. */
   private static final int ENDSIG = 0x6054b50;
 
-  private static final int ENDHDR = 22;
+  private static final int EOCD_SIZE = 22;
+
+  private static final int ZIP64_EOCD_SIZE = 56;
+
+  private static final int ZIP64_EOCD_LOCATOR_SIZE = 20;
+
   /** ZIP64 archive central directory end header signature. */
   private static final int ENDSIG64 = 0x6064b50;
-  /** the maximum size of the end of central directory section in bytes */
-  private static final int MAXIMUM_ZIP_EOCD_SIZE = 64 * 1024 + ENDHDR;
+
+  private static final int MAX_COMMENT_SIZE = 64 * 1024; // 64k
+
+  /** the maximum size of the end of central directory sections in bytes */
+  private static final int MAXIMUM_ZIP_EOCD_SIZE =
+      MAX_COMMENT_SIZE + EOCD_SIZE + ZIP64_EOCD_SIZE + ZIP64_EOCD_LOCATOR_SIZE;
 
   private ZipFile zipFile;
   private ZipEntry zipEntry;
@@ -209,7 +219,6 @@
 
       // First read the 'end of central directory record' in order to find the start of the central
       // directory
-      // The end of central directory record (EOCD) is max comment length (64K) + 22 bytes
       int endOfCdSize = Math.min(MAXIMUM_ZIP_EOCD_SIZE, length);
       int endofCdOffset = length - endOfCdSize;
       randomAccessFile.seek(endofCdOffset);
@@ -217,7 +226,11 @@
       randomAccessFile.readFully(buffer);
 
       int centralDirOffset = findCentralDir(buffer);
-
+      if (centralDirOffset == -1) {
+        // If the zip file contains > 2^16 entries, a Zip64 EOCD is written, and the central
+        // dir offset in the regular EOCD may be -1.
+        centralDirOffset = findCentralDir64(buffer);
+      }
       int offset = centralDirOffset - endofCdOffset;
       if (offset < 0) {
         // read the entire central directory record into memory
@@ -284,7 +297,7 @@
 
   private static int findCentralDir(byte[] buffer) throws IOException {
     // find start of central directory by scanning backwards
-    int scanOffset = buffer.length - ENDHDR;
+    int scanOffset = buffer.length - EOCD_SIZE;
 
     while (true) {
       int val = readInt(buffer, scanOffset);
@@ -305,12 +318,48 @@
     return offsetToCentralDir;
   }
 
+  private static int findCentralDir64(byte[] buffer) throws IOException {
+    // find start of central directory by scanning backwards
+    int scanOffset = buffer.length - EOCD_SIZE - ZIP64_EOCD_LOCATOR_SIZE - ZIP64_EOCD_SIZE;
+
+    while (true) {
+      int val = readInt(buffer, scanOffset);
+      if (val == ENDSIG64) {
+        break;
+      }
+
+      // Ok, keep backing up looking for the ZIP end central directory
+      // signature.
+      --scanOffset;
+      if (scanOffset < 0) {
+        throw new ZipException("ZIP directory not found, not a ZIP archive.");
+      }
+    }
+    // scanOffset is now start of end of central directory record
+    // the 'offset to central dir' data is at position 16 in the record
+    long offsetToCentralDir = readLong(buffer, scanOffset + 48);
+    return (int) offsetToCentralDir;
+  }
+
   /** Read a 32-bit integer from a bytebuffer in little-endian order. */
   private static int readInt(byte[] buffer, int offset) {
     return Ints.fromBytes(
         buffer[offset + 3], buffer[offset + 2], buffer[offset + 1], buffer[offset]);
   }
 
+  /** Read a 64-bit integer from a bytebuffer in little-endian order. */
+  private static long readLong(byte[] buffer, int offset) {
+    return Longs.fromBytes(
+        buffer[offset + 7],
+        buffer[offset + 6],
+        buffer[offset + 5],
+        buffer[offset + 4],
+        buffer[offset + 3],
+        buffer[offset + 2],
+        buffer[offset + 1],
+        buffer[offset]);
+  }
+
   /** Read a 16-bit short from a bytebuffer in little-endian order. */
   private static short readShort(byte[] buffer, int offset) {
     return Shorts.fromBytes(buffer[offset + 1], buffer[offset]);
diff --git a/resources/src/test/java/org/robolectric/res/android/ZipFileROTest.java b/resources/src/test/java/org/robolectric/res/android/ZipFileROTest.java
index eebf365..cf48b21 100644
--- a/resources/src/test/java/org/robolectric/res/android/ZipFileROTest.java
+++ b/resources/src/test/java/org/robolectric/res/android/ZipFileROTest.java
@@ -3,9 +3,12 @@
 import static com.google.common.truth.Truth.assertThat;
 
 import com.google.common.io.ByteStreams;
+import com.google.common.io.Files;
+import java.io.ByteArrayOutputStream;
 import java.io.File;
 import java.io.FileOutputStream;
 import java.io.InputStream;
+import java.util.zip.ZipEntry;
 import java.util.zip.ZipOutputStream;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -63,4 +66,33 @@
     ZipFileRO zipFile = ZipFileRO.open(blob.toString());
     assertThat(zipFile).isNotNull();
   }
+
+  @Test
+  public void testCreateJar() throws Exception {
+    ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
+    ZipOutputStream out = new ZipOutputStream(byteArrayOutputStream);
+    // Write 2^16 + 1 entries, forcing zip64 EOCD to be written.
+    for (int i = 0; i < 65537; i++) {
+      out.putNextEntry(new ZipEntry(Integer.toString(i)));
+      out.closeEntry();
+    }
+    out.close();
+    byte[] zipBytes = byteArrayOutputStream.toByteArray();
+    // Write 0xff for the following fields in the EOCD, which some zip libraries do.
+    // Entries in this disk (2 bytes)
+    // Total Entries (2 byte)
+    // Size of Central Dir (4 bytes)
+    // Offset to Central Dir (4 bytes)
+    // Total: 12 bytes
+    for (int i = 0; i < 12; i++) {
+      zipBytes[zipBytes.length - 3 - i] = (byte) 0xff;
+    }
+    File tmpFile = File.createTempFile("zip64eocd", "zip");
+    Files.write(zipBytes, tmpFile);
+    ZipFileRO zro = ZipFileRO.open(tmpFile.getAbsolutePath());
+    assertThat(zro).isNotNull();
+    assertThat(zro.findEntryByName("0")).isNotNull();
+    assertThat(zro.findEntryByName("65536")).isNotNull();
+    assertThat(zro.findEntryByName("65537")).isNull();
+  }
 }
diff --git a/robolectric/src/main/java/org/robolectric/junit/rules/ExpectedLogMessagesRule.java b/robolectric/src/main/java/org/robolectric/junit/rules/ExpectedLogMessagesRule.java
index 1bcc4cb..5b0b9e9 100644
--- a/robolectric/src/main/java/org/robolectric/junit/rules/ExpectedLogMessagesRule.java
+++ b/robolectric/src/main/java/org/robolectric/junit/rules/ExpectedLogMessagesRule.java
@@ -284,7 +284,7 @@
       return type == log.type
           && !(tag != null ? !tag.equals(log.tag) : log.tag != null)
           && !(msg != null ? !msg.equals(log.msg) : log.msg != null)
-          && !(msgPattern != null ? !msgPattern.equals(log.msgPattern) : log.msgPattern != null)
+          && !(msgPattern != null ? !isEqual(msgPattern, log.msgPattern) : log.msgPattern != null)
           && !(throwableMatcher != null
               ? !throwableMatcher.equals(log.throwableMatcher)
               : log.throwableMatcher != null);
@@ -292,7 +292,7 @@
 
     @Override
     public int hashCode() {
-      return Objects.hash(type, tag, msg, msgPattern, throwableMatcher);
+      return Objects.hash(type, tag, msg, hash(msgPattern), throwableMatcher);
     }
 
     @Override
@@ -313,5 +313,17 @@
           + throwableStr
           + '}';
     }
+
+    /** Returns true if the pattern and flags compiled in a {@link Pattern} were the same. */
+    private static boolean isEqual(Pattern a, Pattern b) {
+      return a != null && b != null
+          ? a.pattern().equals(b.pattern()) && a.flags() == b.flags()
+          : Objects.equals(a, b);
+    }
+
+    /** Returns hash for a {@link Pattern} based on the pattern and flags it was compiled with. */
+    private static int hash(Pattern pattern) {
+      return pattern == null ? 0 : Objects.hash(pattern.pattern(), pattern.flags());
+    }
   }
 }
diff --git a/robolectric/src/main/java/org/robolectric/plugins/GraphicsModeConfigurer.java b/robolectric/src/main/java/org/robolectric/plugins/GraphicsModeConfigurer.java
new file mode 100644
index 0000000..8ff0f61
--- /dev/null
+++ b/robolectric/src/main/java/org/robolectric/plugins/GraphicsModeConfigurer.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.GraphicsMode;
+import org.robolectric.annotation.GraphicsMode.Mode;
+import org.robolectric.pluginapi.config.Configurer;
+
+/** Provides configuration to Robolectric for its @{@link GraphicsMode} annotation. */
+@AutoService(Configurer.class)
+public class GraphicsModeConfigurer implements Configurer<GraphicsMode.Mode> {
+
+  private final Properties systemProperties;
+
+  public GraphicsModeConfigurer(Properties systemProperties) {
+    this.systemProperties = systemProperties;
+  }
+
+  @Override
+  public Class<GraphicsMode.Mode> getConfigClass() {
+    return GraphicsMode.Mode.class;
+  }
+
+  @Nonnull
+  @Override
+  public GraphicsMode.Mode defaultConfig() {
+    return GraphicsMode.Mode.valueOf(
+        systemProperties.getProperty("robolectric.graphicsMode", "LEGACY"));
+  }
+
+  @Override
+  public GraphicsMode.Mode getConfigFor(@Nonnull String packageName) {
+    try {
+      Package pkg = Class.forName(packageName + ".package-info").getPackage();
+      return valueFrom(pkg.getAnnotation(GraphicsMode.class));
+    } catch (ClassNotFoundException e) {
+      // ignore
+    }
+    return null;
+  }
+
+  @Override
+  public GraphicsMode.Mode getConfigFor(@Nonnull Class<?> testClass) {
+    return valueFrom(testClass.getAnnotation(GraphicsMode.class));
+  }
+
+  @Override
+  public GraphicsMode.Mode getConfigFor(@Nonnull Method method) {
+    return valueFrom(method.getAnnotation(GraphicsMode.class));
+  }
+
+  @Nonnull
+  @Override
+  public GraphicsMode.Mode merge(
+      @Nonnull GraphicsMode.Mode parentConfig, @Nonnull GraphicsMode.Mode childConfig) {
+    // just take the childConfig - since GraphicsMode only has a single 'value' attribute
+    return childConfig;
+  }
+
+  private Mode valueFrom(GraphicsMode graphicsMode) {
+    return graphicsMode == null ? null : graphicsMode.value();
+  }
+}
diff --git a/robolectric/src/test/java/org/robolectric/junit/rules/ExpectedLogMessagesRuleTest.java b/robolectric/src/test/java/org/robolectric/junit/rules/ExpectedLogMessagesRuleTest.java
index 44e607d..cd0f301 100644
--- a/robolectric/src/test/java/org/robolectric/junit/rules/ExpectedLogMessagesRuleTest.java
+++ b/robolectric/src/test/java/org/robolectric/junit/rules/ExpectedLogMessagesRuleTest.java
@@ -4,6 +4,7 @@
 
 import android.util.Log;
 import androidx.test.ext.junit.runners.AndroidJUnit4;
+import java.util.regex.Pattern;
 import org.hamcrest.Description;
 import org.hamcrest.TypeSafeMatcher;
 import org.junit.Rule;
@@ -206,4 +207,11 @@
           }
         });
   }
+
+  @Test
+  public void expectLogMessageWithPattern_duplicatePatterns() {
+    Log.e("Mytag", "message1");
+    rule.expectLogMessagePattern(Log.ERROR, "Mytag", Pattern.compile("message1"));
+    rule.expectLogMessagePattern(Log.ERROR, "Mytag", Pattern.compile("message1"));
+  }
 }
diff --git a/robolectric/src/test/java/org/robolectric/plugins/GraphicsModeConfigurerTest.java b/robolectric/src/test/java/org/robolectric/plugins/GraphicsModeConfigurerTest.java
new file mode 100644
index 0000000..dae47d3
--- /dev/null
+++ b/robolectric/src/test/java/org/robolectric/plugins/GraphicsModeConfigurerTest.java
@@ -0,0 +1,21 @@
+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.GraphicsMode;
+import org.robolectric.annotation.GraphicsMode.Mode;
+
+/** Unit tests for methods annotated with @{@link GraphicsMode}. */
+@RunWith(JUnit4.class)
+public class GraphicsModeConfigurerTest {
+  @Test
+  public void defaultConfig() {
+    Properties systemProperties = new Properties();
+    GraphicsModeConfigurer configurer = new GraphicsModeConfigurer(systemProperties);
+    assertThat(configurer.defaultConfig()).isSameInstanceAs(Mode.LEGACY);
+  }
+}
diff --git a/robolectric/src/test/java/org/robolectric/shadows/ResponderLocationBuilderTest.java b/robolectric/src/test/java/org/robolectric/shadows/ResponderLocationBuilderTest.java
new file mode 100644
index 0000000..0478046
--- /dev/null
+++ b/robolectric/src/test/java/org/robolectric/shadows/ResponderLocationBuilderTest.java
@@ -0,0 +1,93 @@
+package org.robolectric.shadows;
+
+import static android.os.Build.VERSION_CODES.Q;
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.assertThrows;
+
+import android.net.wifi.rtt.ResponderLocation;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.annotation.Config;
+
+@RunWith(AndroidJUnit4.class)
+public final class ResponderLocationBuilderTest {
+
+  @Test
+  @Config(minSdk = Q)
+  public void getNewInstance_wouldHaveEmptySubelements() {
+    ResponderLocation responderLocation = ResponderLocationBuilder.newBuilder().build();
+
+    assertThat(responderLocation.isLciSubelementValid()).isFalse();
+    assertThat(responderLocation.isZaxisSubelementValid()).isFalse();
+  }
+
+  @Test
+  @Config(minSdk = Q)
+  public void settingAllLciSubelementFieldsWithNoZaxisFields() {
+    ResponderLocation responderLocation =
+        ResponderLocationBuilder.newBuilder()
+            .setAltitude(498.9)
+            .setAltitudeUncertainty(2.0)
+            .setLatitude(29.1)
+            .setLatitudeUncertainty(3.4)
+            .setLongitude(87.1)
+            .setLongitudeUncertainty(5.4)
+            .setAltitudeType(ResponderLocation.ALTITUDE_UNDEFINED)
+            .setLciVersion(ResponderLocation.LCI_VERSION_1)
+            .setLciRegisteredLocationAgreement(true)
+            .setDatum(1)
+            .build();
+
+    assertThat(responderLocation.isLciSubelementValid()).isTrue();
+    assertThat(responderLocation.isZaxisSubelementValid()).isFalse();
+    assertThrows(IllegalStateException.class, () -> responderLocation.getFloorNumber());
+  }
+
+  @Test
+  @Config(minSdk = Q)
+  public void settingPartsOfLciSubelementFields() {
+    ResponderLocation responderLocation =
+        ResponderLocationBuilder.newBuilder()
+            .setAltitude(498.9)
+            .setAltitudeUncertainty(2.0)
+            .setLatitude(29.1)
+            .setLatitudeUncertainty(3.4)
+            .setLongitude(87.1)
+            .setLongitudeUncertainty(5.4)
+            .setLciVersion(ResponderLocation.LCI_VERSION_1)
+            .setLciRegisteredLocationAgreement(true)
+            .setDatum(1)
+            .build();
+
+    assertThat(responderLocation.isLciSubelementValid()).isFalse();
+    assertThat(responderLocation.isZaxisSubelementValid()).isFalse();
+    assertThrows(IllegalStateException.class, () -> responderLocation.getAltitude());
+    assertThrows(IllegalStateException.class, () -> responderLocation.getFloorNumber());
+  }
+
+  @Test
+  @Config(minSdk = Q)
+  public void settingAllLciSubelementAndZaxisSubelementFields() {
+    ResponderLocation responderLocation =
+        ResponderLocationBuilder.newBuilder()
+            .setAltitude(498.9)
+            .setAltitudeUncertainty(2.0)
+            .setLatitude(29.1)
+            .setLatitudeUncertainty(3.4)
+            .setLongitude(87.1)
+            .setLongitudeUncertainty(5.4)
+            .setAltitudeType(ResponderLocation.ALTITUDE_METERS)
+            .setLciVersion(ResponderLocation.LCI_VERSION_1)
+            .setLciRegisteredLocationAgreement(true)
+            .setDatum(1)
+            .setHeightAboveFloorMeters(2.1)
+            .setHeightAboveFloorUncertaintyMeters(0.1)
+            .setFloorNumber(3.0)
+            .setExpectedToMove(1)
+            .build();
+
+    assertThat(responderLocation.isLciSubelementValid()).isTrue();
+    assertThat(responderLocation.isZaxisSubelementValid()).isTrue();
+  }
+}
diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowAbstractCursorTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowAbstractCursorTest.java
index 100f840..e7bdce6 100644
--- a/robolectric/src/test/java/org/robolectric/shadows/ShadowAbstractCursorTest.java
+++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowAbstractCursorTest.java
@@ -12,7 +12,6 @@
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
-import org.robolectric.Shadows;
 
 @RunWith(AndroidJUnit4.class)
 public class ShadowAbstractCursorTest {
@@ -212,11 +211,10 @@
   @Test
   public void testGetNotificationUri() {
     Uri uri = Uri.parse("content://foo.com");
-    ShadowAbstractCursor shadow = Shadows.shadowOf(cursor);
-    assertThat(shadow.getNotificationUri_Compatibility()).isNull();
+    assertThat(cursor.getNotificationUri()).isNull();
     cursor.setNotificationUri(
         ApplicationProvider.getApplicationContext().getContentResolver(), uri);
-    assertThat(shadow.getNotificationUri_Compatibility()).isEqualTo(uri);
+    assertThat(cursor.getNotificationUri()).isEqualTo(uri);
   }
 
   @Test
diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowAccessibilityNodeInfoTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowAccessibilityNodeInfoTest.java
index 4ac1f2e..14c91f4 100644
--- a/robolectric/src/test/java/org/robolectric/shadows/ShadowAccessibilityNodeInfoTest.java
+++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowAccessibilityNodeInfoTest.java
@@ -245,6 +245,18 @@
   }
 
   @Test
+  @Config(minSdk = P)
+  public void clone_preservesPaneTitle() {
+    String title = "pane title";
+    AccessibilityNodeInfo node = AccessibilityNodeInfo.obtain();
+    node.setPaneTitle(title);
+
+    AccessibilityNodeInfo clone = AccessibilityNodeInfo.obtain(node);
+
+    assertThat(clone.getPaneTitle().toString()).isEqualTo(title);
+  }
+
+  @Test
   public void testGetBoundsInScreen() {
     AccessibilityNodeInfo root = AccessibilityNodeInfo.obtain();
     Rect expected = new Rect(0, 0, 100, 100);
diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowAlarmManagerTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowAlarmManagerTest.java
index c8e1152..7a6ab89 100644
--- a/robolectric/src/test/java/org/robolectric/shadows/ShadowAlarmManagerTest.java
+++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowAlarmManagerTest.java
@@ -1,52 +1,50 @@
 package org.robolectric.shadows;
 
-import static android.app.AlarmManager.INTERVAL_HOUR;
-import static android.app.PendingIntent.FLAG_UPDATE_CURRENT;
-import static android.os.Build.VERSION_CODES.KITKAT;
-import static android.os.Build.VERSION_CODES.LOLLIPOP;
-import static android.os.Build.VERSION_CODES.M;
-import static android.os.Build.VERSION_CODES.N;
-import static android.os.Build.VERSION_CODES.S;
 import static com.google.common.truth.Truth.assertThat;
-import static org.junit.Assert.fail;
+import static org.junit.Assert.assertThrows;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
 import static org.robolectric.Shadows.shadowOf;
 
-import android.app.Activity;
 import android.app.AlarmManager;
 import android.app.AlarmManager.AlarmClockInfo;
 import android.app.AlarmManager.OnAlarmListener;
 import android.app.PendingIntent;
+import android.content.BroadcastReceiver;
 import android.content.Context;
 import android.content.Intent;
+import android.content.IntentFilter;
 import android.os.Build.VERSION_CODES;
-import android.os.Handler;
+import android.os.Looper;
+import android.os.SystemClock;
+import android.os.WorkSource;
 import androidx.test.core.app.ApplicationProvider;
 import androidx.test.ext.junit.runners.AndroidJUnit4;
-import java.util.Date;
+import java.time.Duration;
+import java.util.Objects;
 import java.util.TimeZone;
+import java.util.concurrent.atomic.AtomicReference;
+import javax.annotation.Nullable;
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
-import org.robolectric.Robolectric;
 import org.robolectric.annotation.Config;
+import org.robolectric.shadows.ShadowAlarmManager.ScheduledAlarm;
 
 @RunWith(AndroidJUnit4.class)
 public class ShadowAlarmManagerTest {
 
   private Context context;
-  private Activity activity;
   private AlarmManager alarmManager;
-  private ShadowAlarmManager shadowAlarmManager;
 
   @Before
   public void setUp() {
     context = ApplicationProvider.getApplicationContext();
     alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
-    shadowAlarmManager = shadowOf(alarmManager);
-    activity = Robolectric.setupActivity(Activity.class);
 
-    TimeZone.setDefault(TimeZone.getTimeZone("America/Los_Angeles"));
-    assertThat(TimeZone.getDefault().getID()).isEqualTo("America/Los_Angeles");
+    ShadowAlarmManager.setAutoSchedule(true);
   }
 
   @Test
@@ -64,13 +62,7 @@
   @Test
   @Config(minSdk = VERSION_CODES.M)
   public void setTimeZone_abbreviateTimeZone_ignore() {
-    try {
-      alarmManager.setTimeZone("PST");
-      fail("IllegalArgumentException not thrown");
-    } catch (IllegalArgumentException e) {
-      // expected
-    }
-    assertThat(TimeZone.getDefault().getID()).isEqualTo("America/Los_Angeles");
+    assertThrows(IllegalArgumentException.class, () -> alarmManager.setTimeZone("PST"));
   }
 
   @Test
@@ -83,13 +75,7 @@
   @Test
   @Config(minSdk = VERSION_CODES.M)
   public void setTimeZone_invalidTimeZone_ignore() {
-    try {
-      alarmManager.setTimeZone("-07:00");
-      fail("IllegalArgumentException not thrown");
-    } catch (IllegalArgumentException e) {
-      // expected
-    }
-    assertThat(TimeZone.getDefault().getID()).isEqualTo("America/Los_Angeles");
+    assertThrows(IllegalArgumentException.class, () -> alarmManager.setTimeZone("-07:00"));
   }
 
   @Test
@@ -100,361 +86,623 @@
   }
 
   @Test
-  public void set_shouldRegisterAlarm() {
-    assertThat(shadowAlarmManager.getNextScheduledAlarm()).isNull();
+  public void set_pendingIntent() {
+    Runnable onFire = mock(Runnable.class);
+    try (TestBroadcastListener listener = new TestBroadcastListener(onFire, "action").register()) {
+      alarmManager.set(
+          AlarmManager.ELAPSED_REALTIME,
+          SystemClock.elapsedRealtime() + 10,
+          listener.getPendingIntent());
+
+      ScheduledAlarm alarm = shadowOf(alarmManager).peekNextScheduledAlarm();
+      assertThat(alarm).isNotNull();
+      assertThat(alarm.getType()).isEqualTo(AlarmManager.ELAPSED_REALTIME);
+      assertThat(alarm.getTriggerAtMs()).isEqualTo(SystemClock.elapsedRealtime() + 10);
+
+      shadowOf(Looper.getMainLooper()).idleFor(Duration.ofMillis(10));
+      verify(onFire).run();
+    }
+  }
+
+  @Config(minSdk = VERSION_CODES.N)
+  @Test
+  public void set_alarmListener() {
+    OnAlarmListener onFire = mock(OnAlarmListener.class);
     alarmManager.set(
-        AlarmManager.ELAPSED_REALTIME,
-        0,
-        PendingIntent.getActivity(activity, 0, new Intent(activity, activity.getClass()), 0));
+        AlarmManager.ELAPSED_REALTIME, SystemClock.elapsedRealtime() + 10, "tag", onFire, null);
 
-    ShadowAlarmManager.ScheduledAlarm scheduledAlarm = shadowAlarmManager.getNextScheduledAlarm();
-    assertThat(scheduledAlarm).isNotNull();
-    assertThat(scheduledAlarm.allowWhileIdle).isFalse();
+    ScheduledAlarm alarm = shadowOf(alarmManager).peekNextScheduledAlarm();
+    assertThat(alarm).isNotNull();
+    assertThat(alarm.getType()).isEqualTo(AlarmManager.ELAPSED_REALTIME);
+    assertThat(alarm.getTriggerAtMs()).isEqualTo(SystemClock.elapsedRealtime() + 10);
+    assertThat(alarm.getTag()).isEqualTo("tag");
+
+    shadowOf(Looper.getMainLooper()).idleFor(Duration.ofMillis(10));
+    verify(onFire).onAlarm();
   }
 
   @Test
-  @Config(minSdk = N)
-  public void set_shouldRegisterAlarm_forApi24() {
-    assertThat(shadowAlarmManager.getNextScheduledAlarm()).isNull();
-    OnAlarmListener listener = () -> {};
-    alarmManager.set(AlarmManager.ELAPSED_REALTIME, 0, "tag", listener, null);
-    assertThat(shadowAlarmManager.getNextScheduledAlarm()).isNotNull();
+  public void setRepeating_pendingIntent() {
+    Runnable onFire = mock(Runnable.class);
+    try (TestBroadcastListener listener = new TestBroadcastListener(onFire, "action").register()) {
+      alarmManager.setRepeating(
+          AlarmManager.ELAPSED_REALTIME,
+          SystemClock.elapsedRealtime() + 10,
+          20L,
+          listener.getPendingIntent());
+
+      ScheduledAlarm alarm = shadowOf(alarmManager).peekNextScheduledAlarm();
+      assertThat(alarm).isNotNull();
+      assertThat(alarm.getType()).isEqualTo(AlarmManager.ELAPSED_REALTIME);
+      assertThat(alarm.getTriggerAtMs()).isEqualTo(SystemClock.elapsedRealtime() + 10);
+      assertThat(alarm.getIntervalMs()).isEqualTo(20);
+
+      shadowOf(Looper.getMainLooper()).idleFor(Duration.ofMillis(10));
+      verify(onFire, times(1)).run();
+
+      alarm = shadowOf(alarmManager).peekNextScheduledAlarm();
+      assertThat(alarm).isNotNull();
+      assertThat(alarm.getType()).isEqualTo(AlarmManager.ELAPSED_REALTIME);
+      assertThat(alarm.getTriggerAtMs()).isEqualTo(SystemClock.elapsedRealtime() + 20);
+      assertThat(alarm.getIntervalMs()).isEqualTo(20);
+
+      shadowOf(Looper.getMainLooper()).idleFor(Duration.ofMillis(20));
+      verify(onFire, times(2)).run();
+    }
+
+    shadowOf(Looper.getMainLooper()).idleFor(Duration.ofMillis(20));
+    verify(onFire, times(2)).run();
   }
 
+  @Config(minSdk = VERSION_CODES.KITKAT)
   @Test
-  @Config(minSdk = M)
-  public void setAndAllowWhileIdle_shouldRegisterAlarm() {
-    assertThat(shadowAlarmManager.getNextScheduledAlarm()).isNull();
-    alarmManager.setAndAllowWhileIdle(
-        AlarmManager.ELAPSED_REALTIME,
-        0,
-        PendingIntent.getActivity(activity, 0, new Intent(activity, activity.getClass()), 0));
+  public void setWindow_pendingIntent() {
+    Runnable onFire = mock(Runnable.class);
+    try (TestBroadcastListener listener = new TestBroadcastListener(onFire, "action").register()) {
+      alarmManager.setWindow(
+          AlarmManager.ELAPSED_REALTIME,
+          SystemClock.elapsedRealtime() + 10,
+          20L,
+          listener.getPendingIntent());
 
-    ShadowAlarmManager.ScheduledAlarm scheduledAlarm = shadowAlarmManager.getNextScheduledAlarm();
-    assertThat(scheduledAlarm).isNotNull();
-    assertThat(scheduledAlarm.allowWhileIdle).isTrue();
+      ScheduledAlarm alarm = shadowOf(alarmManager).peekNextScheduledAlarm();
+      assertThat(alarm).isNotNull();
+      assertThat(alarm.getType()).isEqualTo(AlarmManager.ELAPSED_REALTIME);
+      assertThat(alarm.getTriggerAtMs()).isEqualTo(SystemClock.elapsedRealtime() + 10);
+      assertThat(alarm.getWindowLengthMs()).isEqualTo(20);
+
+      shadowOf(Looper.getMainLooper()).idleFor(Duration.ofMillis(10));
+      verify(onFire).run();
+    }
   }
 
+  @Config(minSdk = VERSION_CODES.N)
   @Test
-  @Config(minSdk = M)
-  public void setExactAndAllowWhileIdle_shouldRegisterAlarm() {
-    assertThat(shadowAlarmManager.getNextScheduledAlarm()).isNull();
-    alarmManager.setExactAndAllowWhileIdle(
-        AlarmManager.ELAPSED_REALTIME,
-        0,
-        PendingIntent.getActivity(activity, 0, new Intent(activity, activity.getClass()), 0));
-
-    ShadowAlarmManager.ScheduledAlarm scheduledAlarm = shadowAlarmManager.getNextScheduledAlarm();
-    assertThat(scheduledAlarm).isNotNull();
-    assertThat(scheduledAlarm.allowWhileIdle).isTrue();
-  }
-
-  @Test
-  @Config(minSdk = KITKAT)
-  public void setExact_shouldRegisterAlarm_forApi19() {
-    assertThat(shadowAlarmManager.getNextScheduledAlarm()).isNull();
-    alarmManager.setExact(
-        AlarmManager.ELAPSED_REALTIME,
-        0,
-        PendingIntent.getActivity(activity, 0, new Intent(activity, activity.getClass()), 0));
-    assertThat(shadowAlarmManager.getNextScheduledAlarm()).isNotNull();
-  }
-
-  @Test
-  @Config(minSdk = N)
-  public void setExact_shouldRegisterAlarm_forApi124() {
-    assertThat(shadowAlarmManager.getNextScheduledAlarm()).isNull();
-    OnAlarmListener listener = () -> {};
-    alarmManager.setExact(AlarmManager.ELAPSED_REALTIME, 0, "tag", listener, null);
-    assertThat(shadowAlarmManager.getNextScheduledAlarm()).isNotNull();
-  }
-
-  @Test
-  @Config(minSdk = KITKAT)
-  public void setWindow_shouldRegisterAlarm_forApi19() {
-    assertThat(shadowAlarmManager.getNextScheduledAlarm()).isNull();
+  public void setWindow_alarmListener() {
+    OnAlarmListener onFire = mock(OnAlarmListener.class);
     alarmManager.setWindow(
         AlarmManager.ELAPSED_REALTIME,
-        0,
-        1,
-        PendingIntent.getActivity(activity, 0, new Intent(activity, activity.getClass()), 0));
-    assertThat(shadowAlarmManager.getNextScheduledAlarm()).isNotNull();
+        SystemClock.elapsedRealtime() + 10,
+        20L,
+        "tag",
+        onFire,
+        null);
+
+    ScheduledAlarm alarm = shadowOf(alarmManager).peekNextScheduledAlarm();
+    assertThat(alarm).isNotNull();
+    assertThat(alarm.getType()).isEqualTo(AlarmManager.ELAPSED_REALTIME);
+    assertThat(alarm.getTriggerAtMs()).isEqualTo(SystemClock.elapsedRealtime() + 10);
+    assertThat(alarm.getWindowLengthMs()).isEqualTo(20);
+    assertThat(alarm.getTag()).isEqualTo("tag");
+
+    shadowOf(Looper.getMainLooper()).idleFor(Duration.ofMillis(10));
+    verify(onFire).onAlarm();
   }
 
+  @Config(minSdk = VERSION_CODES.S)
   @Test
-  @Config(minSdk = N)
-  public void setWindow_shouldRegisterAlarm_forApi24() {
-    assertThat(shadowAlarmManager.getNextScheduledAlarm()).isNull();
-    OnAlarmListener listener = () -> {};
-    alarmManager.setWindow(AlarmManager.ELAPSED_REALTIME, 0, 1, "tag", listener, null);
-    assertThat(shadowAlarmManager.getNextScheduledAlarm()).isNotNull();
-  }
-
-  @Test
-  public void setRepeating_shouldRegisterAlarm() {
-    assertThat(shadowAlarmManager.getNextScheduledAlarm()).isNull();
-    alarmManager.setRepeating(
+  public void setPrioritized_alarmListener() {
+    OnAlarmListener onFire = mock(OnAlarmListener.class);
+    alarmManager.setPrioritized(
         AlarmManager.ELAPSED_REALTIME,
-        0,
-        INTERVAL_HOUR,
-        PendingIntent.getActivity(activity, 0, new Intent(activity, activity.getClass()), 0));
-    assertThat(shadowAlarmManager.getNextScheduledAlarm()).isNotNull();
+        SystemClock.elapsedRealtime() + 10,
+        20L,
+        "tag",
+        Runnable::run,
+        onFire);
+
+    ScheduledAlarm alarm = shadowOf(alarmManager).peekNextScheduledAlarm();
+    assertThat(alarm).isNotNull();
+    assertThat(alarm.getType()).isEqualTo(AlarmManager.ELAPSED_REALTIME);
+    assertThat(alarm.getTriggerAtMs()).isEqualTo(SystemClock.elapsedRealtime() + 10);
+    assertThat(alarm.getWindowLengthMs()).isEqualTo(20);
+    assertThat(alarm.getTag()).isEqualTo("tag");
+
+    shadowOf(Looper.getMainLooper()).idleFor(Duration.ofMillis(10));
+    verify(onFire).onAlarm();
   }
 
+  @Config(minSdk = VERSION_CODES.KITKAT)
   @Test
-  public void set_shouldReplaceAlarmsWithSameIntentReceiver() {
+  public void setExact_pendingIntent() {
+    Runnable onFire = mock(Runnable.class);
+    try (TestBroadcastListener listener = new TestBroadcastListener(onFire, "action").register()) {
+      alarmManager.setExact(
+          AlarmManager.ELAPSED_REALTIME,
+          SystemClock.elapsedRealtime() + 10,
+          listener.getPendingIntent());
+
+      ScheduledAlarm alarm = shadowOf(alarmManager).peekNextScheduledAlarm();
+      assertThat(alarm).isNotNull();
+      assertThat(alarm.getType()).isEqualTo(AlarmManager.ELAPSED_REALTIME);
+      assertThat(alarm.getTriggerAtMs()).isEqualTo(SystemClock.elapsedRealtime() + 10);
+
+      shadowOf(Looper.getMainLooper()).idleFor(Duration.ofMillis(10));
+      verify(onFire).run();
+    }
+  }
+
+  @Config(minSdk = VERSION_CODES.N)
+  @Test
+  public void setExact_alarmListener() {
+    OnAlarmListener onFire = mock(OnAlarmListener.class);
+    alarmManager.setExact(
+        AlarmManager.ELAPSED_REALTIME, SystemClock.elapsedRealtime() + 10, "tag", onFire, null);
+
+    ScheduledAlarm alarm = shadowOf(alarmManager).peekNextScheduledAlarm();
+    assertThat(alarm).isNotNull();
+    assertThat(alarm.getType()).isEqualTo(AlarmManager.ELAPSED_REALTIME);
+    assertThat(alarm.getTriggerAtMs()).isEqualTo(SystemClock.elapsedRealtime() + 10);
+    assertThat(alarm.getTag()).isEqualTo("tag");
+
+    shadowOf(Looper.getMainLooper()).idleFor(Duration.ofMillis(10));
+    verify(onFire).onAlarm();
+  }
+
+  @Config(minSdk = VERSION_CODES.LOLLIPOP)
+  @Test
+  public void setAlarmClock_pendingIntent() {
+    AlarmClockInfo alarmClockInfo =
+        new AlarmClockInfo(
+            SystemClock.elapsedRealtime() + 10,
+            PendingIntent.getBroadcast(context, 0, new Intent("show"), 0));
+
+    Runnable onFire = mock(Runnable.class);
+    try (TestBroadcastListener listener = new TestBroadcastListener(onFire, "action").register()) {
+      alarmManager.setAlarmClock(alarmClockInfo, listener.getPendingIntent());
+
+      ScheduledAlarm alarm = shadowOf(alarmManager).peekNextScheduledAlarm();
+      assertThat(alarm).isNotNull();
+      assertThat(alarm.getType()).isEqualTo(AlarmManager.RTC_WAKEUP);
+      assertThat(alarm.getTriggerAtMs()).isEqualTo(SystemClock.elapsedRealtime() + 10);
+      assertThat(alarm.getAlarmClockInfo()).isEqualTo(alarmClockInfo);
+
+      shadowOf(Looper.getMainLooper()).idleFor(Duration.ofMillis(10));
+      verify(onFire).run();
+    }
+  }
+
+  @Config(minSdk = VERSION_CODES.KITKAT)
+  @Test
+  public void set_pendingIntent_workSource() {
+    Runnable onFire = mock(Runnable.class);
+    try (TestBroadcastListener listener = new TestBroadcastListener(onFire, "action").register()) {
+      alarmManager.set(
+          AlarmManager.ELAPSED_REALTIME,
+          SystemClock.elapsedRealtime() + 10,
+          20L,
+          0L,
+          listener.getPendingIntent(),
+          new WorkSource());
+
+      ScheduledAlarm alarm = shadowOf(alarmManager).peekNextScheduledAlarm();
+      assertThat(alarm).isNotNull();
+      assertThat(alarm.getType()).isEqualTo(AlarmManager.ELAPSED_REALTIME);
+      assertThat(alarm.getTriggerAtMs()).isEqualTo(SystemClock.elapsedRealtime() + 10);
+      assertThat(alarm.getWindowLengthMs()).isEqualTo(20);
+      assertThat(alarm.getIntervalMs()).isEqualTo(0);
+      assertThat(alarm.getWorkSource()).isEqualTo(new WorkSource());
+
+      shadowOf(Looper.getMainLooper()).idleFor(Duration.ofMillis(10));
+      verify(onFire).run();
+    }
+  }
+
+  @Config(minSdk = VERSION_CODES.N)
+  @Test
+  public void set_alarmListener_workSource() {
+    OnAlarmListener onFire = mock(OnAlarmListener.class);
     alarmManager.set(
         AlarmManager.ELAPSED_REALTIME,
-        500,
-        PendingIntent.getActivity(activity, 0, new Intent(activity, activity.getClass()), 0));
+        SystemClock.elapsedRealtime() + 10,
+        20L,
+        0L,
+        "tag",
+        onFire,
+        null,
+        new WorkSource());
+
+    ScheduledAlarm alarm = shadowOf(alarmManager).peekNextScheduledAlarm();
+    assertThat(alarm).isNotNull();
+    assertThat(alarm.getType()).isEqualTo(AlarmManager.ELAPSED_REALTIME);
+    assertThat(alarm.getTriggerAtMs()).isEqualTo(SystemClock.elapsedRealtime() + 10);
+    assertThat(alarm.getWindowLengthMs()).isEqualTo(20);
+    assertThat(alarm.getIntervalMs()).isEqualTo(0);
+    assertThat(alarm.getTag()).isEqualTo("tag");
+    assertThat(alarm.getWorkSource()).isEqualTo(new WorkSource());
+
+    shadowOf(Looper.getMainLooper()).idleFor(Duration.ofMillis(10));
+    verify(onFire).onAlarm();
+  }
+
+  @Config(minSdk = VERSION_CODES.N)
+  @Test
+  public void set_alarmListener_workSource_noTag() {
+    OnAlarmListener onFire = mock(OnAlarmListener.class);
     alarmManager.set(
         AlarmManager.ELAPSED_REALTIME,
-        1000,
-        PendingIntent.getActivity(activity, 0, new Intent(activity, activity.getClass()), 0));
-    assertThat(shadowAlarmManager.getScheduledAlarms()).hasSize(1);
+        SystemClock.elapsedRealtime() + 10,
+        20L,
+        0L,
+        onFire,
+        null,
+        new WorkSource());
+
+    ScheduledAlarm alarm = shadowOf(alarmManager).peekNextScheduledAlarm();
+    assertThat(alarm).isNotNull();
+    assertThat(alarm.getType()).isEqualTo(AlarmManager.ELAPSED_REALTIME);
+    assertThat(alarm.getTriggerAtMs()).isEqualTo(SystemClock.elapsedRealtime() + 10);
+    assertThat(alarm.getWindowLengthMs()).isEqualTo(20);
+    assertThat(alarm.getIntervalMs()).isEqualTo(0);
+    assertThat(alarm.getWorkSource()).isEqualTo(new WorkSource());
+
+    shadowOf(Looper.getMainLooper()).idleFor(Duration.ofMillis(10));
+    verify(onFire).onAlarm();
+  }
+
+  @Config(minSdk = VERSION_CODES.S)
+  @Test
+  public void setExact_alarmListener_workSource() {
+    OnAlarmListener onFire = mock(OnAlarmListener.class);
+    alarmManager.setExact(
+        AlarmManager.ELAPSED_REALTIME,
+        SystemClock.elapsedRealtime() + 10,
+        "tag",
+        Runnable::run,
+        new WorkSource(),
+        onFire);
+
+    ScheduledAlarm alarm = shadowOf(alarmManager).peekNextScheduledAlarm();
+    assertThat(alarm).isNotNull();
+    assertThat(alarm.getType()).isEqualTo(AlarmManager.ELAPSED_REALTIME);
+    assertThat(alarm.getTriggerAtMs()).isEqualTo(SystemClock.elapsedRealtime() + 10);
+    assertThat(alarm.getTag()).isEqualTo("tag");
+    assertThat(alarm.getWorkSource()).isEqualTo(new WorkSource());
+
+    shadowOf(Looper.getMainLooper()).idleFor(Duration.ofMillis(10));
+    verify(onFire).onAlarm();
   }
 
   @Test
-  public void set_shouldReplaceDuplicates() {
+  public void setInexactRepeating_pendingIntent() {
+    Runnable onFire = mock(Runnable.class);
+    try (TestBroadcastListener listener = new TestBroadcastListener(onFire, "action").register()) {
+      alarmManager.setInexactRepeating(
+          AlarmManager.ELAPSED_REALTIME,
+          SystemClock.elapsedRealtime() + 10,
+          20L,
+          listener.getPendingIntent());
+
+      ScheduledAlarm alarm = shadowOf(alarmManager).peekNextScheduledAlarm();
+      assertThat(alarm).isNotNull();
+      assertThat(alarm.getType()).isEqualTo(AlarmManager.ELAPSED_REALTIME);
+      assertThat(alarm.getTriggerAtMs()).isEqualTo(SystemClock.elapsedRealtime() + 10);
+      assertThat(alarm.getIntervalMs()).isEqualTo(20);
+
+      shadowOf(Looper.getMainLooper()).idleFor(Duration.ofMillis(10));
+      verify(onFire, times(1)).run();
+
+      shadowOf(Looper.getMainLooper()).idleFor(Duration.ofMillis(20));
+      verify(onFire, times(2)).run();
+    }
+
+    shadowOf(Looper.getMainLooper()).idleFor(Duration.ofMillis(20));
+    verify(onFire, times(2)).run();
+  }
+
+  @Config(minSdk = VERSION_CODES.M)
+  @Test
+  public void setAndAllowWhileIdle_pendingIntent() {
+    Runnable onFire = mock(Runnable.class);
+    try (TestBroadcastListener listener = new TestBroadcastListener(onFire, "action").register()) {
+      alarmManager.setAndAllowWhileIdle(
+          AlarmManager.ELAPSED_REALTIME,
+          SystemClock.elapsedRealtime() + 10,
+          listener.getPendingIntent());
+
+      ScheduledAlarm alarm = shadowOf(alarmManager).peekNextScheduledAlarm();
+      assertThat(alarm).isNotNull();
+      assertThat(alarm.getType()).isEqualTo(AlarmManager.ELAPSED_REALTIME);
+      assertThat(alarm.getTriggerAtMs()).isEqualTo(SystemClock.elapsedRealtime() + 10);
+      assertThat(alarm.isAllowWhileIdle()).isTrue();
+
+      shadowOf(Looper.getMainLooper()).idleFor(Duration.ofMillis(10));
+      verify(onFire, times(1)).run();
+    }
+  }
+
+  @Config(minSdk = VERSION_CODES.M)
+  @Test
+  public void setExactAndAllowWhileIdle_pendingIntent() {
+    Runnable onFire = mock(Runnable.class);
+    try (TestBroadcastListener listener = new TestBroadcastListener(onFire, "action").register()) {
+      alarmManager.setExactAndAllowWhileIdle(
+          AlarmManager.ELAPSED_REALTIME,
+          SystemClock.elapsedRealtime() + 10,
+          listener.getPendingIntent());
+
+      ScheduledAlarm alarm = shadowOf(alarmManager).peekNextScheduledAlarm();
+      assertThat(alarm).isNotNull();
+      assertThat(alarm.getType()).isEqualTo(AlarmManager.ELAPSED_REALTIME);
+      assertThat(alarm.getTriggerAtMs()).isEqualTo(SystemClock.elapsedRealtime() + 10);
+      assertThat(alarm.isAllowWhileIdle()).isTrue();
+
+      shadowOf(Looper.getMainLooper()).idleFor(Duration.ofMillis(10));
+      verify(onFire, times(1)).run();
+    }
+  }
+
+  @Test
+  public void cancel_pendingIntent() {
+    Runnable onFire1 = mock(Runnable.class);
+    Runnable onFire2 = mock(Runnable.class);
+    try (TestBroadcastListener listener1 =
+            new TestBroadcastListener(onFire1, "action1").register();
+        TestBroadcastListener listener2 =
+            new TestBroadcastListener(onFire2, "action2").register()) {
+      alarmManager.set(
+          AlarmManager.ELAPSED_REALTIME,
+          SystemClock.elapsedRealtime() + 20,
+          listener1.getPendingIntent());
+      alarmManager.set(
+          AlarmManager.ELAPSED_REALTIME,
+          SystemClock.elapsedRealtime() + 10,
+          listener2.getPendingIntent());
+
+      assertThat(shadowOf(alarmManager).getScheduledAlarms()).hasSize(2);
+      ScheduledAlarm alarm = shadowOf(alarmManager).peekNextScheduledAlarm();
+      assertThat(alarm).isNotNull();
+      assertThat(alarm.getTriggerAtMs()).isEqualTo(SystemClock.elapsedRealtime() + 10);
+
+      alarmManager.cancel(listener2.getPendingIntent());
+
+      assertThat(shadowOf(alarmManager).getScheduledAlarms()).hasSize(1);
+      alarm = shadowOf(alarmManager).peekNextScheduledAlarm();
+      assertThat(alarm).isNotNull();
+      assertThat(alarm.getTriggerAtMs()).isEqualTo(SystemClock.elapsedRealtime() + 20);
+
+      alarmManager.cancel(listener1.getPendingIntent());
+
+      assertThat(shadowOf(alarmManager).getScheduledAlarms()).isEmpty();
+      assertThat(shadowOf(alarmManager).peekNextScheduledAlarm()).isNull();
+
+      shadowOf(Looper.getMainLooper()).idleFor(Duration.ofMillis(20));
+      verify(onFire1, never()).run();
+      verify(onFire2, never()).run();
+    }
+  }
+
+  @Config(minSdk = VERSION_CODES.N)
+  @Test
+  public void cancel_alarmListener() {
+    OnAlarmListener onFire1 = mock(OnAlarmListener.class);
+    OnAlarmListener onFire2 = mock(OnAlarmListener.class);
     alarmManager.set(
-        AlarmManager.ELAPSED_REALTIME,
-        0,
-        PendingIntent.getActivity(activity, 0, new Intent(activity, activity.getClass()), 0));
+        AlarmManager.ELAPSED_REALTIME, SystemClock.elapsedRealtime() + 20, "tag", onFire1, null);
     alarmManager.set(
-        AlarmManager.ELAPSED_REALTIME,
-        0,
-        PendingIntent.getActivity(activity, 0, new Intent(activity, activity.getClass()), 0));
-    assertThat(shadowAlarmManager.getScheduledAlarms()).hasSize(1);
+        AlarmManager.ELAPSED_REALTIME, SystemClock.elapsedRealtime() + 10, "tag", onFire2, null);
+
+    assertThat(shadowOf(alarmManager).getScheduledAlarms()).hasSize(2);
+    ScheduledAlarm alarm = shadowOf(alarmManager).peekNextScheduledAlarm();
+    assertThat(alarm).isNotNull();
+    assertThat(alarm.getTriggerAtMs()).isEqualTo(SystemClock.elapsedRealtime() + 10);
+
+    alarmManager.cancel(onFire2);
+
+    assertThat(shadowOf(alarmManager).getScheduledAlarms()).hasSize(1);
+    alarm = shadowOf(alarmManager).peekNextScheduledAlarm();
+    assertThat(alarm).isNotNull();
+    assertThat(alarm.getTriggerAtMs()).isEqualTo(SystemClock.elapsedRealtime() + 20);
+
+    alarmManager.cancel(onFire1);
+
+    assertThat(shadowOf(alarmManager).getScheduledAlarms()).isEmpty();
+    assertThat(shadowOf(alarmManager).peekNextScheduledAlarm()).isNull();
+
+    shadowOf(Looper.getMainLooper()).idleFor(Duration.ofMillis(20));
+    verify(onFire1, never()).onAlarm();
+    verify(onFire2, never()).onAlarm();
   }
 
   @Test
-  public void setRepeating_shouldReplaceDuplicates() {
-    alarmManager.setRepeating(
-        AlarmManager.ELAPSED_REALTIME,
-        0,
-        INTERVAL_HOUR,
-        PendingIntent.getActivity(activity, 0, new Intent(activity, activity.getClass()), 0));
-    alarmManager.setRepeating(
-        AlarmManager.ELAPSED_REALTIME,
-        0,
-        INTERVAL_HOUR,
-        PendingIntent.getActivity(activity, 0, new Intent(activity, activity.getClass()), 0));
-    assertThat(shadowAlarmManager.getScheduledAlarms()).hasSize(1);
-  }
-
-  @Test
-  @SuppressWarnings("JavaUtilDate")
-  public void shouldSupportGetNextScheduledAlarm() {
-    assertThat(shadowAlarmManager.getNextScheduledAlarm()).isNull();
-
-    long now = new Date().getTime();
-    Intent intent = new Intent(activity, activity.getClass());
-    PendingIntent pendingIntent = PendingIntent.getActivity(activity, 0, intent, 0);
-    alarmManager.set(AlarmManager.ELAPSED_REALTIME, now, pendingIntent);
-
-    ShadowAlarmManager.ScheduledAlarm scheduledAlarm = shadowAlarmManager.getNextScheduledAlarm();
-    assertThat(shadowAlarmManager.getNextScheduledAlarm()).isNull();
-    assertScheduledAlarm(now, pendingIntent, scheduledAlarm);
-  }
-
-  @Test
-  @SuppressWarnings("JavaUtilDate")
-  public void getNextScheduledAlarm_shouldReturnRepeatingAlarms() {
-    assertThat(shadowAlarmManager.getNextScheduledAlarm()).isNull();
-
-    long now = new Date().getTime();
-    Intent intent = new Intent(activity, activity.getClass());
-    PendingIntent pendingIntent = PendingIntent.getActivity(activity, 0, intent, 0);
-    alarmManager.setRepeating(AlarmManager.ELAPSED_REALTIME, now, INTERVAL_HOUR, pendingIntent);
-
-    ShadowAlarmManager.ScheduledAlarm scheduledAlarm = shadowAlarmManager.getNextScheduledAlarm();
-    assertThat(shadowAlarmManager.getNextScheduledAlarm()).isNull();
-    assertRepeatingScheduledAlarm(now, INTERVAL_HOUR, pendingIntent, scheduledAlarm);
-  }
-
-  @Test
-  @SuppressWarnings("JavaUtilDate")
-  public void peekNextScheduledAlarm_shouldReturnNextAlarm() {
-    assertThat(shadowAlarmManager.getNextScheduledAlarm()).isNull();
-
-    long now = new Date().getTime();
-    Intent intent = new Intent(activity, activity.getClass());
-    PendingIntent pendingIntent = PendingIntent.getActivity(activity, 0, intent, 0);
-    alarmManager.set(AlarmManager.ELAPSED_REALTIME, now, pendingIntent);
-
-    ShadowAlarmManager.ScheduledAlarm scheduledAlarm = shadowAlarmManager.peekNextScheduledAlarm();
-    assertThat(shadowAlarmManager.peekNextScheduledAlarm()).isNotNull();
-    assertScheduledAlarm(now, pendingIntent, scheduledAlarm);
-  }
-
-  @Test
-  public void cancel_removesMatchingPendingIntents() {
-    Intent intent = new Intent(context, String.class);
-    PendingIntent pendingIntent =
-        PendingIntent.getBroadcast(context, 0, intent, FLAG_UPDATE_CURRENT);
-    alarmManager.set(AlarmManager.RTC, 1337, pendingIntent);
-
-    Intent intent2 = new Intent(context, Integer.class);
-    PendingIntent pendingIntent2 =
-        PendingIntent.getBroadcast(context, 0, intent2, FLAG_UPDATE_CURRENT);
-    alarmManager.set(AlarmManager.RTC, 1337, pendingIntent2);
-
-    assertThat(shadowAlarmManager.getScheduledAlarms()).hasSize(2);
-
-    Intent intent3 = new Intent(context, String.class);
-    PendingIntent pendingIntent3 =
-        PendingIntent.getBroadcast(context, 0, intent3, FLAG_UPDATE_CURRENT);
-    alarmManager.cancel(pendingIntent3);
-
-    assertThat(shadowAlarmManager.getScheduledAlarms()).hasSize(1);
-  }
-
-  @Test
-  public void cancel_removesMatchingPendingIntentsWithActions() {
-    Intent newIntent = new Intent("someAction");
-    PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 0, newIntent, 0);
-
-    alarmManager.set(AlarmManager.RTC, 1337, pendingIntent);
-    assertThat(shadowAlarmManager.getScheduledAlarms()).hasSize(1);
-
-    alarmManager.cancel(PendingIntent.getBroadcast(context, 0, new Intent("anotherAction"), 0));
-    assertThat(shadowAlarmManager.getScheduledAlarms()).hasSize(1);
-
-    alarmManager.cancel(PendingIntent.getBroadcast(context, 0, new Intent("someAction"), 0));
-    assertThat(shadowAlarmManager.getScheduledAlarms()).hasSize(0);
-  }
-
-  @Test
-  public void schedule_useRequestCodeToMatchExistingPendingIntents() {
-    Intent intent = new Intent("ACTION!");
-    PendingIntent pI = PendingIntent.getService(context, 1, intent, 0);
-    alarmManager.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, 10, pI);
-
-    PendingIntent pI2 = PendingIntent.getService(context, 2, intent, 0);
-    alarmManager.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, 10, pI2);
-
-    assertThat(shadowAlarmManager.getScheduledAlarms()).hasSize(2);
-  }
-
-  @Test
-  public void cancel_useRequestCodeToMatchExistingPendingIntents() {
-    Intent intent = new Intent("ACTION!");
-    PendingIntent pI = PendingIntent.getService(context, 1, intent, 0);
-    alarmManager.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, 10, pI);
-
-    PendingIntent pI2 = PendingIntent.getService(context, 2, intent, 0);
-    alarmManager.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, 10, pI2);
-
-    assertThat(shadowAlarmManager.getScheduledAlarms()).hasSize(2);
-
-    alarmManager.cancel(pI);
-    assertThat(shadowAlarmManager.getScheduledAlarms()).hasSize(1);
-    assertThat(shadowAlarmManager.getNextScheduledAlarm().operation).isEqualTo(pI2);
-  }
-
-  @Test
-  @Config(minSdk = N)
-  public void cancel_removesMatchingListeners() {
-    Intent intent = new Intent("ACTION!");
-    PendingIntent pI = PendingIntent.getService(context, 1, intent, 0);
-    OnAlarmListener listener1 = () -> {};
-    OnAlarmListener listener2 = () -> {};
-    Handler handler = new Handler();
-
-    alarmManager.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, 20, "tag", listener1, handler);
-    alarmManager.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, 30, "tag", listener2, handler);
-    alarmManager.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, 40, pI);
-    assertThat(shadowAlarmManager.getScheduledAlarms()).hasSize(3);
-
-    alarmManager.cancel(listener1);
-    assertThat(shadowAlarmManager.getScheduledAlarms()).hasSize(2);
-    assertThat(shadowAlarmManager.peekNextScheduledAlarm().onAlarmListener).isEqualTo(listener2);
-    assertThat(shadowAlarmManager.peekNextScheduledAlarm().handler).isEqualTo(handler);
-  }
-
-  @Test
-  @Config(minSdk = LOLLIPOP)
-  public void getNextAlarmClockInfo() {
-    assertThat(alarmManager.getNextAlarmClock()).isNull();
-    assertThat(shadowAlarmManager.peekNextScheduledAlarm()).isNull();
-
-    // Schedule an alarm.
-    PendingIntent show = PendingIntent.getBroadcast(context, 0, new Intent("showAction"), 0);
-    PendingIntent operation = PendingIntent.getBroadcast(context, 0, new Intent("opAction"), 0);
-    AlarmClockInfo info = new AlarmClockInfo(1000, show);
-    alarmManager.setAlarmClock(info, operation);
-
-    AlarmClockInfo next = alarmManager.getNextAlarmClock();
-    assertThat(next).isNotNull();
-    assertThat(next.getTriggerTime()).isEqualTo(1000);
-    assertThat(next.getShowIntent()).isSameInstanceAs(show);
-    assertThat(shadowAlarmManager.peekNextScheduledAlarm().operation).isSameInstanceAs(operation);
-
-    // Schedule another alarm sooner.
-    PendingIntent show2 = PendingIntent.getBroadcast(context, 0, new Intent("showAction2"), 0);
-    PendingIntent operation2 = PendingIntent.getBroadcast(context, 0, new Intent("opAction2"), 0);
-    AlarmClockInfo info2 = new AlarmClockInfo(500, show2);
-    alarmManager.setAlarmClock(info2, operation2);
-
-    next = alarmManager.getNextAlarmClock();
-    assertThat(next).isNotNull();
-    assertThat(next.getTriggerTime()).isEqualTo(500);
-    assertThat(next.getShowIntent()).isSameInstanceAs(show2);
-    assertThat(shadowAlarmManager.peekNextScheduledAlarm().operation).isSameInstanceAs(operation2);
-
-    // Remove the soonest alarm.
-    alarmManager.cancel(operation2);
-
-    next = alarmManager.getNextAlarmClock();
-    assertThat(next).isNotNull();
-    assertThat(next.getTriggerTime()).isEqualTo(1000);
-    assertThat(next.getShowIntent()).isSameInstanceAs(show);
-    assertThat(shadowAlarmManager.peekNextScheduledAlarm().operation).isSameInstanceAs(operation);
-
-    // Remove the sole alarm.
-    alarmManager.cancel(operation);
-
-    assertThat(alarmManager.getNextAlarmClock()).isNull();
-    assertThat(shadowAlarmManager.peekNextScheduledAlarm()).isNull();
-  }
-
-  @Test
-  @Config(minSdk = S)
-  public void canScheduleExactAlarms_default_returnsTrue() {
+  @Config(minSdk = VERSION_CODES.S)
+  public void canScheduleExactAlarms() {
     assertThat(alarmManager.canScheduleExactAlarms()).isFalse();
-  }
 
-  @Test
-  @Config(minSdk = S)
-  public void canScheduleExactAlarms_setCanScheduleExactAlarms_returnsTrue() {
     ShadowAlarmManager.setCanScheduleExactAlarms(true);
-
     assertThat(alarmManager.canScheduleExactAlarms()).isTrue();
-  }
 
-  @Test
-  @Config(minSdk = S)
-  public void canScheduleExactAlarms_setCannotScheduleExactAlarms_returnsFalse() {
     ShadowAlarmManager.setCanScheduleExactAlarms(false);
-
     assertThat(alarmManager.canScheduleExactAlarms()).isFalse();
   }
 
-  private void assertScheduledAlarm(
-      long now, PendingIntent pendingIntent, ShadowAlarmManager.ScheduledAlarm scheduledAlarm) {
-    assertRepeatingScheduledAlarm(now, 0L, pendingIntent, scheduledAlarm);
+  @Test
+  @Config(minSdk = VERSION_CODES.LOLLIPOP)
+  public void getNextAlarmClockInfo() {
+    AlarmClockInfo alarmClockInfo1 =
+        new AlarmClockInfo(
+            SystemClock.elapsedRealtime() + 10,
+            PendingIntent.getBroadcast(context, 0, new Intent("show1"), 0));
+    AlarmClockInfo alarmClockInfo2 =
+        new AlarmClockInfo(
+            SystemClock.elapsedRealtime() + 5,
+            PendingIntent.getBroadcast(context, 0, new Intent("show2"), 0));
+
+    alarmManager.setAlarmClock(
+        alarmClockInfo1, PendingIntent.getBroadcast(context, 0, new Intent("fire1"), 0));
+    alarmManager.setAlarmClock(
+        alarmClockInfo2, PendingIntent.getBroadcast(context, 0, new Intent("fire2"), 0));
+    assertThat(alarmManager.getNextAlarmClock()).isEqualTo(alarmClockInfo2);
+
+    shadowOf(Looper.getMainLooper()).idleFor(Duration.ofMillis(5));
+    assertThat(alarmManager.getNextAlarmClock()).isEqualTo(alarmClockInfo1);
+
+    shadowOf(Looper.getMainLooper()).idleFor(Duration.ofMillis(5));
+    assertThat(alarmManager.getNextAlarmClock()).isNull();
   }
 
-  private void assertRepeatingScheduledAlarm(
-      long now,
-      long interval,
-      PendingIntent pendingIntent,
-      ShadowAlarmManager.ScheduledAlarm scheduledAlarm) {
-    assertThat(scheduledAlarm).isNotNull();
-    assertThat(scheduledAlarm.operation).isNotNull();
-    assertThat(scheduledAlarm.operation).isSameInstanceAs(pendingIntent);
-    assertThat(scheduledAlarm.type).isEqualTo(AlarmManager.ELAPSED_REALTIME);
-    assertThat(scheduledAlarm.triggerAtTime).isEqualTo(now);
-    assertThat(scheduledAlarm.interval).isEqualTo(interval);
+  @Test
+  public void replace_pendingIntent() {
+    Runnable onFire = mock(Runnable.class);
+    try (TestBroadcastListener listener = new TestBroadcastListener(onFire, "action").register()) {
+      alarmManager.set(
+          AlarmManager.ELAPSED_REALTIME,
+          SystemClock.elapsedRealtime() + 10,
+          listener.getPendingIntent());
+      alarmManager.set(
+          AlarmManager.ELAPSED_REALTIME_WAKEUP,
+          SystemClock.elapsedRealtime() + 20,
+          listener.getPendingIntent());
+
+      ScheduledAlarm alarm = shadowOf(alarmManager).peekNextScheduledAlarm();
+      assertThat(alarm).isNotNull();
+      assertThat(alarm.getType()).isEqualTo(AlarmManager.ELAPSED_REALTIME_WAKEUP);
+      assertThat(alarm.getTriggerAtMs()).isEqualTo(SystemClock.elapsedRealtime() + 20);
+
+      shadowOf(Looper.getMainLooper()).idleFor(Duration.ofMillis(10));
+      verify(onFire, never()).run();
+
+      shadowOf(Looper.getMainLooper()).idleFor(Duration.ofMillis(10));
+      verify(onFire).run();
+    }
+  }
+
+  @Config(minSdk = VERSION_CODES.N)
+  @Test
+  public void replace_alarmListener() {
+    OnAlarmListener onFire = mock(OnAlarmListener.class);
+    alarmManager.set(
+        AlarmManager.ELAPSED_REALTIME, SystemClock.elapsedRealtime() + 10, "tag", onFire, null);
+    alarmManager.set(
+        AlarmManager.ELAPSED_REALTIME_WAKEUP,
+        SystemClock.elapsedRealtime() + 20,
+        "tag1",
+        onFire,
+        null);
+
+    ScheduledAlarm alarm = shadowOf(alarmManager).peekNextScheduledAlarm();
+    assertThat(alarm).isNotNull();
+    assertThat(alarm.getType()).isEqualTo(AlarmManager.ELAPSED_REALTIME_WAKEUP);
+    assertThat(alarm.getTriggerAtMs()).isEqualTo(SystemClock.elapsedRealtime() + 20);
+    assertThat(alarm.getTag()).isEqualTo("tag1");
+
+    shadowOf(Looper.getMainLooper()).idleFor(Duration.ofMillis(10));
+    verify(onFire, never()).onAlarm();
+
+    shadowOf(Looper.getMainLooper()).idleFor(Duration.ofMillis(10));
+    verify(onFire).onAlarm();
+  }
+
+  @Test
+  public void pastTime() {
+    Runnable onFire = mock(Runnable.class);
+    try (TestBroadcastListener listener = new TestBroadcastListener(onFire, "action").register()) {
+      alarmManager.set(
+          AlarmManager.ELAPSED_REALTIME,
+          SystemClock.elapsedRealtime() - 10,
+          listener.getPendingIntent());
+
+      ScheduledAlarm alarm = shadowOf(alarmManager).peekNextScheduledAlarm();
+      assertThat(alarm).isNotNull();
+      assertThat(alarm.getType()).isEqualTo(AlarmManager.ELAPSED_REALTIME);
+      assertThat(alarm.getTriggerAtMs()).isEqualTo(SystemClock.elapsedRealtime() - 10);
+
+      shadowOf(Looper.getMainLooper()).idle();
+      verify(onFire).run();
+
+      assertThat(shadowOf(alarmManager).peekNextScheduledAlarm()).isNull();
+    }
+  }
+
+  @Config(minSdk = VERSION_CODES.N)
+  @Test
+  public void reentrant() {
+    AtomicReference<OnAlarmListener> listenerRef = new AtomicReference<>();
+    listenerRef.set(
+        () ->
+            alarmManager.set(
+                AlarmManager.ELAPSED_REALTIME,
+                SystemClock.elapsedRealtime() + 10,
+                "tag",
+                listenerRef.get(),
+                null));
+    alarmManager.set(
+        AlarmManager.ELAPSED_REALTIME_WAKEUP,
+        SystemClock.elapsedRealtime() + 10,
+        "tag",
+        listenerRef.get(),
+        null);
+
+    ScheduledAlarm alarm = shadowOf(alarmManager).peekNextScheduledAlarm();
+    assertThat(alarm).isNotNull();
+    assertThat(alarm.getType()).isEqualTo(AlarmManager.ELAPSED_REALTIME_WAKEUP);
+    assertThat(alarm.getTriggerAtMs()).isEqualTo(SystemClock.elapsedRealtime() + 10);
+    assertThat(alarm.getTag()).isEqualTo("tag");
+
+    shadowOf(Looper.getMainLooper()).idleFor(Duration.ofMillis(10));
+
+    alarm = shadowOf(alarmManager).peekNextScheduledAlarm();
+    assertThat(alarm).isNotNull();
+    assertThat(alarm.getType()).isEqualTo(AlarmManager.ELAPSED_REALTIME);
+    assertThat(alarm.getTriggerAtMs()).isEqualTo(SystemClock.elapsedRealtime() + 10);
+    assertThat(alarm.getTag()).isEqualTo("tag");
+  }
+
+  private class TestBroadcastListener extends BroadcastReceiver implements AutoCloseable {
+
+    private final Runnable alarm;
+    private final String action;
+
+    @Nullable private PendingIntent pendingIntent;
+
+    TestBroadcastListener(Runnable alarm, String action) {
+      this.alarm = alarm;
+      this.action = action;
+    }
+
+    TestBroadcastListener register() {
+      pendingIntent = PendingIntent.getBroadcast(context, 0, new Intent(action), 0);
+      context.registerReceiver(this, new IntentFilter(action));
+      return this;
+    }
+
+    PendingIntent getPendingIntent() {
+      return Objects.requireNonNull(pendingIntent);
+    }
+
+    @Override
+    public void close() {
+      context.unregisterReceiver(this);
+      if (pendingIntent != null) {
+        pendingIntent.cancel();
+      }
+    }
+
+    @Override
+    public void onReceive(Context context, Intent intent) {
+      if (Objects.equals(action, intent.getAction())) {
+        alarm.run();
+      }
+    }
   }
 }
diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowAnimationUtilsTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowAnimationUtilsTest.java
index 7ecc1f8..a429301 100644
--- a/robolectric/src/test/java/org/robolectric/shadows/ShadowAnimationUtilsTest.java
+++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowAnimationUtilsTest.java
@@ -5,29 +5,24 @@
 import android.R;
 import android.app.Activity;
 import android.view.animation.AnimationUtils;
-import android.view.animation.LayoutAnimationController;
 import androidx.test.ext.junit.runners.AndroidJUnit4;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.robolectric.Robolectric;
-import org.robolectric.Shadows;
 
 @RunWith(AndroidJUnit4.class)
 public class ShadowAnimationUtilsTest {
 
   @Test
   public void loadAnimation_shouldCreateAnimation() {
-    assertThat(AnimationUtils.loadAnimation(Robolectric.setupActivity(Activity.class), R.anim.fade_in)).isNotNull();
+    assertThat(
+            AnimationUtils.loadAnimation(Robolectric.setupActivity(Activity.class), R.anim.fade_in))
+        .isNotNull();
   }
 
   @Test
   public void loadLayoutAnimation_shouldCreateAnimation() {
-    assertThat(AnimationUtils.loadLayoutAnimation(Robolectric.setupActivity(Activity.class), 1)).isNotNull();
-  }
-
-  @Test
-  public void getLoadedFromResourceId_forAnimationController_shouldReturnAnimationResourceId() {
-    final LayoutAnimationController anim = AnimationUtils.loadLayoutAnimation(Robolectric.setupActivity(Activity.class), R.anim.fade_in);
-    assertThat(Shadows.shadowOf(anim).getLoadedFromResourceId()).isEqualTo(R.anim.fade_in);
+    assertThat(AnimationUtils.loadLayoutAnimation(Robolectric.setupActivity(Activity.class), 1))
+        .isNotNull();
   }
 }
diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowAudioManagerTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowAudioManagerTest.java
index f5a29a8..b798a74 100644
--- a/robolectric/src/test/java/org/robolectric/shadows/ShadowAudioManagerTest.java
+++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowAudioManagerTest.java
@@ -18,6 +18,7 @@
 
 import android.content.Context;
 import android.media.AudioAttributes;
+import android.media.AudioDeviceCallback;
 import android.media.AudioDeviceInfo;
 import android.media.AudioFormat;
 import android.media.AudioManager;
@@ -31,6 +32,7 @@
 import java.lang.reflect.Field;
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.Collections;
 import java.util.List;
 import org.junit.Before;
 import org.junit.Test;
@@ -399,6 +401,240 @@
 
   @Test
   @Config(minSdk = M)
+  public void registerAudioDeviceCallback_availableDevices_onAudioDevicesAddedCallback()
+      throws Exception {
+    AudioDeviceInfo device = createAudioDevice(AudioDeviceInfo.TYPE_BLUETOOTH_SCO);
+    shadowOf(audioManager).setInputDevices(Collections.singletonList(device));
+
+    AudioDeviceCallback callback = mock(AudioDeviceCallback.class);
+    audioManager.registerAudioDeviceCallback(callback, /* handler= */ null);
+
+    verify(callback).onAudioDevicesAdded(new AudioDeviceInfo[] {device});
+  }
+
+  @Test
+  @Config(minSdk = M)
+  public void setInputDevices_withCallbackRegistered_noNotificationCallback() throws Exception {
+    AudioDeviceCallback callback = mock(AudioDeviceCallback.class);
+    audioManager.registerAudioDeviceCallback(callback, /* handler= */ null);
+    verify(callback).onAudioDevicesAdded(new AudioDeviceInfo[] {}); // initial registration
+
+    AudioDeviceInfo device = createAudioDevice(AudioDeviceInfo.TYPE_BLUETOOTH_SCO);
+    shadowOf(audioManager).setInputDevices(Collections.singletonList(device));
+
+    verifyNoMoreInteractions(callback);
+  }
+
+  @Test
+  @Config(minSdk = M)
+  public void addInputDevice_callbackRegisteredUnregistered_noNotificationCallback()
+      throws Exception {
+    AudioDeviceCallback callback = mock(AudioDeviceCallback.class);
+    audioManager.registerAudioDeviceCallback(callback, /* handler= */ null);
+    audioManager.unregisterAudioDeviceCallback(callback);
+    verify(callback).onAudioDevicesAdded(new AudioDeviceInfo[] {}); // initial registration
+
+    AudioDeviceInfo device = createAudioDevice(AudioDeviceInfo.TYPE_BLUETOOTH_SCO);
+    shadowOf(audioManager).addInputDevice(device, /* notifyAudioDeviceCallbacks= */ true);
+
+    verifyNoMoreInteractions(callback);
+  }
+
+  @Test
+  @Config(minSdk = M)
+  public void addInputDevice_withCallbackRegisteredAndNoDevice_deviceAddedAndNotifiesCallback()
+      throws Exception {
+    AudioDeviceCallback callback = mock(AudioDeviceCallback.class);
+    audioManager.registerAudioDeviceCallback(callback, /* handler= */ null);
+    verify(callback).onAudioDevicesAdded(new AudioDeviceInfo[] {}); // initial registration
+
+    AudioDeviceInfo device = createAudioDevice(AudioDeviceInfo.TYPE_BLUETOOTH_SCO);
+    shadowOf(audioManager).addInputDevice(device, /* notifyAudioDeviceCallbacks= */ true);
+
+    verify(callback).onAudioDevicesAdded(new AudioDeviceInfo[] {device});
+  }
+
+  @Test
+  @Config(minSdk = M)
+  public void
+      addInputDeviceNoCallbackNotification_withCallbackRegisteredAndNoDevice_noNotificationCallback()
+          throws Exception {
+    AudioDeviceCallback callback = mock(AudioDeviceCallback.class);
+    audioManager.registerAudioDeviceCallback(callback, /* handler= */ null);
+    verify(callback).onAudioDevicesAdded(new AudioDeviceInfo[] {}); // initial registration
+
+    AudioDeviceInfo device = createAudioDevice(AudioDeviceInfo.TYPE_BLUETOOTH_SCO);
+    shadowOf(audioManager).addInputDevice(device, /* notifyAudioDeviceCallbacks= */ false);
+
+    verifyNoMoreInteractions(callback);
+  }
+
+  @Test
+  @Config(minSdk = M)
+  public void addInputDevice_withCallbackRegisteredAndDevicePresent_noNotificationCallback()
+      throws Exception {
+    AudioDeviceCallback callback = mock(AudioDeviceCallback.class);
+    audioManager.registerAudioDeviceCallback(callback, /* handler= */ null);
+    verify(callback).onAudioDevicesAdded(new AudioDeviceInfo[] {}); // initial registration
+    AudioDeviceInfo device = createAudioDevice(AudioDeviceInfo.TYPE_BLUETOOTH_SCO);
+    shadowOf(audioManager).setInputDevices(Collections.singletonList(device));
+
+    shadowOf(audioManager).addInputDevice(device, /* notifyAudioDeviceCallbacks= */ true);
+
+    verifyNoMoreInteractions(callback);
+  }
+
+  @Test
+  @Config(minSdk = M)
+  public void
+      removeInputDevice_withCallbackRegisteredAndDevicePresent_deviceRemovedAndNotifiesCallback()
+          throws Exception {
+    AudioDeviceCallback callback = mock(AudioDeviceCallback.class);
+    audioManager.registerAudioDeviceCallback(callback, /* handler= */ null);
+    verify(callback).onAudioDevicesAdded(new AudioDeviceInfo[] {}); // initial registration
+    AudioDeviceInfo device = createAudioDevice(AudioDeviceInfo.TYPE_BLUETOOTH_SCO);
+    shadowOf(audioManager).setInputDevices(Collections.singletonList(device));
+
+    shadowOf(audioManager).removeInputDevice(device, /* notifyAudioDeviceCallbacks= */ true);
+
+    verify(callback).onAudioDevicesRemoved(new AudioDeviceInfo[] {device});
+  }
+
+  @Test
+  @Config(minSdk = M)
+  public void
+      removeInputDeviceNoCallbackNotification_withCallbackRegisteredAndDevicePresent_noNotificationCallback()
+          throws Exception {
+    AudioDeviceCallback callback = mock(AudioDeviceCallback.class);
+    audioManager.registerAudioDeviceCallback(callback, /* handler= */ null);
+    verify(callback).onAudioDevicesAdded(new AudioDeviceInfo[] {}); // initial registration
+    AudioDeviceInfo device = createAudioDevice(AudioDeviceInfo.TYPE_BLUETOOTH_SCO);
+    shadowOf(audioManager).setInputDevices(Collections.singletonList(device));
+
+    shadowOf(audioManager).removeInputDevice(device, /* notifyAudioDeviceCallbacks= */ false);
+
+    verifyNoMoreInteractions(callback);
+  }
+
+  @Test
+  @Config(minSdk = M)
+  public void removeInputDevice_withCallbackRegisteredAndNoDevice_noNotificationCallback()
+      throws Exception {
+    AudioDeviceCallback callback = mock(AudioDeviceCallback.class);
+    audioManager.registerAudioDeviceCallback(callback, /* handler= */ null);
+    verify(callback).onAudioDevicesAdded(new AudioDeviceInfo[] {}); // initial registration
+
+    AudioDeviceInfo device = createAudioDevice(AudioDeviceInfo.TYPE_BLUETOOTH_SCO);
+    shadowOf(audioManager).removeInputDevice(device, /* notifyAudioDeviceCallbacks= */ true);
+
+    verifyNoMoreInteractions(callback);
+  }
+
+  @Test
+  @Config(minSdk = M)
+  public void setOutputDevices_withCallbackRegistered_noNotificationCallback() throws Exception {
+    AudioDeviceCallback callback = mock(AudioDeviceCallback.class);
+    audioManager.registerAudioDeviceCallback(callback, /* handler= */ null);
+    verify(callback).onAudioDevicesAdded(new AudioDeviceInfo[] {}); // initial registration
+
+    AudioDeviceInfo device = createAudioDevice(AudioDeviceInfo.TYPE_BLUETOOTH_SCO);
+    shadowOf(audioManager).setOutputDevices(Collections.singletonList(device));
+
+    verifyNoMoreInteractions(callback);
+  }
+
+  @Test
+  @Config(minSdk = M)
+  public void addOutputDevice_withCallbackRegisteredAndNoDevice_deviceAddedAndNotifiesCallback()
+      throws Exception {
+    AudioDeviceCallback callback = mock(AudioDeviceCallback.class);
+    audioManager.registerAudioDeviceCallback(callback, /* handler= */ null);
+    verify(callback).onAudioDevicesAdded(new AudioDeviceInfo[] {}); // initial registration
+
+    AudioDeviceInfo device = createAudioDevice(AudioDeviceInfo.TYPE_BLUETOOTH_SCO);
+    shadowOf(audioManager).addOutputDevice(device, /* notifyAudioDeviceCallbacks= */ true);
+
+    verify(callback).onAudioDevicesAdded(new AudioDeviceInfo[] {device});
+  }
+
+  @Test
+  @Config(minSdk = M)
+  public void
+      addOutputDeviceNoCallbackNotification_withCallbackRegisteredAndNoDevice_noNotificationCallback()
+          throws Exception {
+    AudioDeviceCallback callback = mock(AudioDeviceCallback.class);
+    audioManager.registerAudioDeviceCallback(callback, /* handler= */ null);
+    verify(callback).onAudioDevicesAdded(new AudioDeviceInfo[] {}); // initial registration
+
+    AudioDeviceInfo device = createAudioDevice(AudioDeviceInfo.TYPE_BLUETOOTH_SCO);
+    shadowOf(audioManager).addOutputDevice(device, /* notifyAudioDeviceCallbacks= */ false);
+
+    verifyNoMoreInteractions(callback);
+  }
+
+  @Test
+  @Config(minSdk = M)
+  public void addOutputDevice_withCallbackRegisteredAndDevicePresent_noNotificationCallback()
+      throws Exception {
+    AudioDeviceCallback callback = mock(AudioDeviceCallback.class);
+    audioManager.registerAudioDeviceCallback(callback, /* handler= */ null);
+    verify(callback).onAudioDevicesAdded(new AudioDeviceInfo[] {}); // initial registration
+    AudioDeviceInfo device = createAudioDevice(AudioDeviceInfo.TYPE_BLUETOOTH_SCO);
+    shadowOf(audioManager).setOutputDevices(Collections.singletonList(device));
+
+    shadowOf(audioManager).addOutputDevice(device, /* notifyAudioDeviceCallbacks= */ true);
+
+    verifyNoMoreInteractions(callback);
+  }
+
+  @Test
+  @Config(minSdk = M)
+  public void
+      removeOutputDevice_withCallbackRegisteredAndDevicePresent_deviceRemovedAndNotifiesCallback()
+          throws Exception {
+    AudioDeviceCallback callback = mock(AudioDeviceCallback.class);
+    audioManager.registerAudioDeviceCallback(callback, /* handler= */ null);
+    verify(callback).onAudioDevicesAdded(new AudioDeviceInfo[] {}); // initial registration
+    AudioDeviceInfo device = createAudioDevice(AudioDeviceInfo.TYPE_BLUETOOTH_SCO);
+    shadowOf(audioManager).setOutputDevices(Collections.singletonList(device));
+
+    shadowOf(audioManager).removeOutputDevice(device, /* notifyAudioDeviceCallbacks= */ true);
+
+    verify(callback).onAudioDevicesRemoved(new AudioDeviceInfo[] {device});
+  }
+
+  @Test
+  @Config(minSdk = M)
+  public void
+      removeOutputDeviceNoCallbackNotification_withCallbackRegisteredAndDevicePresent_noNotificationCallback()
+          throws Exception {
+    AudioDeviceCallback callback = mock(AudioDeviceCallback.class);
+    audioManager.registerAudioDeviceCallback(callback, /* handler= */ null);
+    verify(callback).onAudioDevicesAdded(new AudioDeviceInfo[] {}); // initial registration
+    AudioDeviceInfo device = createAudioDevice(AudioDeviceInfo.TYPE_BLUETOOTH_SCO);
+    shadowOf(audioManager).setOutputDevices(Collections.singletonList(device));
+
+    shadowOf(audioManager).removeOutputDevice(device, /* notifyAudioDeviceCallbacks= */ false);
+
+    verifyNoMoreInteractions(callback);
+  }
+
+  @Test
+  @Config(minSdk = M)
+  public void removeOutputDevice_withCallbackRegisteredAndNoDevice_noNotificationCallback()
+      throws Exception {
+    AudioDeviceCallback callback = mock(AudioDeviceCallback.class);
+    audioManager.registerAudioDeviceCallback(callback, /* handler= */ null);
+    verify(callback).onAudioDevicesAdded(new AudioDeviceInfo[] {}); // initial registration
+
+    AudioDeviceInfo device = createAudioDevice(AudioDeviceInfo.TYPE_BLUETOOTH_SCO);
+    shadowOf(audioManager).removeOutputDevice(device, /* notifyAudioDeviceCallbacks= */ true);
+
+    verifyNoMoreInteractions(callback);
+  }
+
+  @Test
+  @Config(minSdk = M)
   public void getDevices_criteriaInputs_getsAllInputDevices() throws Exception {
     AudioDeviceInfo scoDevice = createAudioDevice(AudioDeviceInfo.TYPE_BLUETOOTH_SCO);
     AudioDeviceInfo a2dpDevice = createAudioDevice(AudioDeviceInfo.TYPE_BLUETOOTH_A2DP);
diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowBitmapTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowBitmapTest.java
index 2e5be13..dfe336b 100644
--- a/robolectric/src/test/java/org/robolectric/shadows/ShadowBitmapTest.java
+++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowBitmapTest.java
@@ -149,13 +149,13 @@
   @Test
   public void shouldCreateBitmapWithMatrix() {
     Bitmap originalBitmap = create("Original bitmap");
-    shadowOf(originalBitmap).setWidth(200);
-    shadowOf(originalBitmap).setHeight(200);
+    ((ShadowLegacyBitmap) Shadow.extract(originalBitmap)).setWidth(200);
+    ((ShadowLegacyBitmap) Shadow.extract(originalBitmap)).setHeight(200);
     Matrix m = new Matrix();
     m.postRotate(90);
     Bitmap newBitmap = Bitmap.createBitmap(originalBitmap, 0, 0, 100, 50, m, true);
 
-    ShadowBitmap shadowBitmap = shadowOf(newBitmap);
+    ShadowLegacyBitmap shadowBitmap = Shadow.extract(newBitmap);
     assertThat(shadowBitmap.getDescription())
         .isEqualTo(
             "Original bitmap at (0,0) with width 100 and height 50"
@@ -246,8 +246,8 @@
   public void shouldCopyBitmap() {
     Bitmap bitmap = Shadow.newInstanceOf(Bitmap.class);
     Bitmap bitmapCopy = bitmap.copy(Bitmap.Config.ARGB_8888, true);
-    assertThat(shadowOf(bitmapCopy).getConfig()).isEqualTo(Bitmap.Config.ARGB_8888);
-    assertThat(shadowOf(bitmapCopy).isMutable()).isTrue();
+    assertThat(bitmapCopy.getConfig()).isEqualTo(Bitmap.Config.ARGB_8888);
+    assertThat(bitmapCopy.isMutable()).isTrue();
   }
 
   @Test(expected = NullPointerException.class)
@@ -538,7 +538,7 @@
   @Test
   public void compress_shouldSucceedForNullPixelData() {
     Bitmap bitmap = Shadow.newInstanceOf(Bitmap.class);
-    ShadowBitmap shadowBitmap = Shadow.extract(bitmap);
+    ShadowLegacyBitmap shadowBitmap = Shadow.extract(bitmap);
     shadowBitmap.setWidth(100);
     shadowBitmap.setHeight(100);
     ByteArrayOutputStream stream = new ByteArrayOutputStream();
@@ -548,15 +548,15 @@
   @Config(sdk = O)
   @Test
   public void getBytesPerPixel_O() {
-    assertThat(ShadowBitmap.getBytesPerPixel(Bitmap.Config.RGBA_F16)).isEqualTo(8);
+    assertThat(ShadowLegacyBitmap.getBytesPerPixel(Bitmap.Config.RGBA_F16)).isEqualTo(8);
   }
 
   @Test
   public void getBytesPerPixel_preO() {
-    assertThat(ShadowBitmap.getBytesPerPixel(Bitmap.Config.ARGB_8888)).isEqualTo(4);
-    assertThat(ShadowBitmap.getBytesPerPixel(Bitmap.Config.RGB_565)).isEqualTo(2);
-    assertThat(ShadowBitmap.getBytesPerPixel(Bitmap.Config.ARGB_4444)).isEqualTo(2);
-    assertThat(ShadowBitmap.getBytesPerPixel(Bitmap.Config.ALPHA_8)).isEqualTo(1);
+    assertThat(ShadowLegacyBitmap.getBytesPerPixel(Bitmap.Config.ARGB_8888)).isEqualTo(4);
+    assertThat(ShadowLegacyBitmap.getBytesPerPixel(Bitmap.Config.RGB_565)).isEqualTo(2);
+    assertThat(ShadowLegacyBitmap.getBytesPerPixel(Bitmap.Config.ARGB_4444)).isEqualTo(2);
+    assertThat(ShadowLegacyBitmap.getBytesPerPixel(Bitmap.Config.ALPHA_8)).isEqualTo(1);
   }
 
   @Test(expected = RuntimeException.class)
@@ -642,9 +642,7 @@
   @Test(expected = IllegalStateException.class)
   public void reconfigure_withHardwareBitmap_validDimensionsAndConfig_throws() {
     Bitmap original = Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888);
-    ShadowBitmap shadowBitmap = Shadow.extract(original);
-    shadowBitmap.setConfig(Bitmap.Config.HARDWARE);
-
+    original.setConfig(Bitmap.Config.HARDWARE);
     original.reconfigure(100, 100, Bitmap.Config.ARGB_8888);
   }
 
@@ -814,15 +812,17 @@
   private void createScaledBitmap_expectedUpSize(boolean filter) {
     Bitmap bitmap = Bitmap.createBitmap(10, 10, Bitmap.Config.ARGB_8888);
     Bitmap scaledBitmap = Bitmap.createScaledBitmap(bitmap, 32, 32, filter);
-    assertThat(Shadows.shadowOf(scaledBitmap).getBufferedImage().getWidth()).isEqualTo(32);
-    assertThat(Shadows.shadowOf(scaledBitmap).getBufferedImage().getHeight()).isEqualTo(32);
+    ShadowLegacyBitmap shadowBitmap = Shadow.extract(scaledBitmap);
+    assertThat(shadowBitmap.getBufferedImage().getWidth()).isEqualTo(32);
+    assertThat(shadowBitmap.getBufferedImage().getHeight()).isEqualTo(32);
   }
 
   private void createScaledBitmap_expectedDownSize(boolean filter) {
     Bitmap bitmap = Bitmap.createBitmap(32, 32, Bitmap.Config.ARGB_8888);
     Bitmap scaledBitmap = Bitmap.createScaledBitmap(bitmap, 10, 10, filter);
-    assertThat(Shadows.shadowOf(scaledBitmap).getBufferedImage().getWidth()).isEqualTo(10);
-    assertThat(Shadows.shadowOf(scaledBitmap).getBufferedImage().getHeight()).isEqualTo(10);
+    ShadowLegacyBitmap shadowBitmap = Shadow.extract(scaledBitmap);
+    assertThat(shadowBitmap.getBufferedImage().getWidth()).isEqualTo(10);
+    assertThat(shadowBitmap.getBufferedImage().getHeight()).isEqualTo(10);
   }
 
   private void createScaledBitmap_drawOnScaled(boolean filter) {
diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowBluetoothAdapterTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowBluetoothAdapterTest.java
index 69d7e7b..10cb148 100644
--- a/robolectric/src/test/java/org/robolectric/shadows/ShadowBluetoothAdapterTest.java
+++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowBluetoothAdapterTest.java
@@ -495,6 +495,51 @@
   }
 
   @Test
+  public void closeProfileProxy_severalCallersObserving_allNotified() {
+    BluetoothProfile mockProxy = mock(BluetoothProfile.class);
+    BluetoothProfile.ServiceListener mockServiceListener =
+        mock(BluetoothProfile.ServiceListener.class);
+    BluetoothProfile.ServiceListener mockServiceListener2 =
+        mock(BluetoothProfile.ServiceListener.class);
+    shadowOf(bluetoothAdapter).setProfileProxy(MOCK_PROFILE1, mockProxy);
+
+    bluetoothAdapter.getProfileProxy(
+        RuntimeEnvironment.getApplication(), mockServiceListener, MOCK_PROFILE1);
+    bluetoothAdapter.getProfileProxy(
+        RuntimeEnvironment.getApplication(), mockServiceListener2, MOCK_PROFILE1);
+
+    bluetoothAdapter.closeProfileProxy(MOCK_PROFILE1, mockProxy);
+
+    verify(mockServiceListener).onServiceDisconnected(MOCK_PROFILE1);
+    verify(mockServiceListener2).onServiceDisconnected(MOCK_PROFILE1);
+  }
+
+  @Test
+  public void closeProfileProxy_severalCallersObservingAndClosedTwice_allNotifiedOnce() {
+    BluetoothProfile mockProxy = mock(BluetoothProfile.class);
+    BluetoothProfile.ServiceListener mockServiceListener =
+        mock(BluetoothProfile.ServiceListener.class);
+    BluetoothProfile.ServiceListener mockServiceListener2 =
+        mock(BluetoothProfile.ServiceListener.class);
+    shadowOf(bluetoothAdapter).setProfileProxy(MOCK_PROFILE1, mockProxy);
+
+    bluetoothAdapter.getProfileProxy(
+        RuntimeEnvironment.getApplication(), mockServiceListener, MOCK_PROFILE1);
+    bluetoothAdapter.getProfileProxy(
+        RuntimeEnvironment.getApplication(), mockServiceListener2, MOCK_PROFILE1);
+
+    bluetoothAdapter.closeProfileProxy(MOCK_PROFILE1, mockProxy);
+    verify(mockServiceListener).onServiceConnected(MOCK_PROFILE1, mockProxy);
+    verify(mockServiceListener2).onServiceConnected(MOCK_PROFILE1, mockProxy);
+    verify(mockServiceListener).onServiceDisconnected(MOCK_PROFILE1);
+    verify(mockServiceListener2).onServiceDisconnected(MOCK_PROFILE1);
+
+    bluetoothAdapter.closeProfileProxy(MOCK_PROFILE1, mockProxy);
+    verifyNoMoreInteractions(mockServiceListener);
+    verifyNoMoreInteractions(mockServiceListener2);
+  }
+
+  @Test
   public void closeProfileProxy_reversesSetProfileProxy() {
     BluetoothProfile mockProxy = mock(BluetoothProfile.class);
     BluetoothProfile.ServiceListener mockServiceListener =
diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowBluetoothGattTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowBluetoothGattTest.java
index a76dbc7..caed178 100644
--- a/robolectric/src/test/java/org/robolectric/shadows/ShadowBluetoothGattTest.java
+++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowBluetoothGattTest.java
@@ -1,13 +1,20 @@
 package org.robolectric.shadows;
 
 import static android.os.Build.VERSION_CODES.JELLY_BEAN_MR2;
+import static android.os.Build.VERSION_CODES.O;
 import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.assertThrows;
 import static org.robolectric.Shadows.shadowOf;
 
 import android.bluetooth.BluetoothDevice;
 import android.bluetooth.BluetoothGatt;
 import android.bluetooth.BluetoothGattCallback;
+import android.bluetooth.BluetoothGattCharacteristic;
+import android.bluetooth.BluetoothGattService;
+import android.bluetooth.BluetoothProfile;
 import androidx.test.ext.junit.runners.AndroidJUnit4;
+import java.util.UUID;
+import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.robolectric.annotation.Config;
@@ -17,30 +24,451 @@
 @Config(minSdk = JELLY_BEAN_MR2)
 public class ShadowBluetoothGattTest {
 
+  private static final byte[] CHARACTERISTIC_VALUE = new byte[] {'a', 'b', 'c'};
+  private static final int INITIAL_VALUE = -99;
   private static final String MOCK_MAC_ADDRESS = "00:11:22:33:AA:BB";
+  private static final String ACTION_CONNECTION = "CONNECT/DISCONNECT";
+  private static final String ACTION_DISCOVER = "DISCOVER";
+  private static final String ACTION_READ = "READ";
+  private static final String ACTION_WRITE = "WRITE";
+
+  private int resultStatus = INITIAL_VALUE;
+  private int resultState = INITIAL_VALUE;
+  private String resultAction;
+  private BluetoothGattCharacteristic resultCharacteristic;
+  private BluetoothGatt bluetoothGatt;
+
+  private static final BluetoothGattService service1 =
+      new BluetoothGattService(
+          UUID.fromString("00000000-0000-0000-0000-0000000000A1"),
+          BluetoothGattService.SERVICE_TYPE_PRIMARY);
+  private static final BluetoothGattService service2 =
+      new BluetoothGattService(
+          UUID.fromString("00000000-0000-0000-0000-0000000000A2"),
+          BluetoothGattService.SERVICE_TYPE_SECONDARY);
+
+  private final BluetoothGattCallback callback =
+      new BluetoothGattCallback() {
+        @Override
+        public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) {
+          resultStatus = status;
+          resultState = newState;
+          resultAction = ACTION_CONNECTION;
+        }
+
+        @Override
+        public void onServicesDiscovered(BluetoothGatt gatt, int status) {
+          resultStatus = status;
+          resultAction = ACTION_DISCOVER;
+        }
+
+        @Override
+        public void onCharacteristicRead(
+            BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {
+          resultStatus = status;
+          resultCharacteristic = characteristic;
+          resultAction = ACTION_READ;
+        }
+
+        @Override
+        public void onCharacteristicWrite(
+            BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {
+          resultStatus = status;
+          resultCharacteristic = characteristic;
+          resultAction = ACTION_WRITE;
+        }
+      };
+
+  private final BluetoothGattCharacteristic characteristicWithReadProperty =
+      new BluetoothGattCharacteristic(
+          UUID.fromString("00000000-0000-0000-0000-0000000000A3"),
+          BluetoothGattCharacteristic.PROPERTY_READ,
+          BluetoothGattCharacteristic.PERMISSION_READ);
+
+  private final BluetoothGattCharacteristic characteristicWithWriteProperties =
+      new BluetoothGattCharacteristic(
+          UUID.fromString("00000000-0000-0000-0000-0000000000A4"),
+          BluetoothGattCharacteristic.PROPERTY_WRITE
+              | BluetoothGattCharacteristic.PROPERTY_WRITE_NO_RESPONSE,
+          BluetoothGattCharacteristic.PERMISSION_WRITE);
+
+  @Before
+  public void setUp() throws Exception {
+    BluetoothDevice bluetoothDevice = ShadowBluetoothDevice.newInstance(MOCK_MAC_ADDRESS);
+    bluetoothGatt = ShadowBluetoothGatt.newInstance(bluetoothDevice);
+  }
 
   @Test
   public void canCreateBluetoothGattViaNewInstance() {
-    BluetoothDevice bluetoothDevice = ShadowBluetoothDevice.newInstance(MOCK_MAC_ADDRESS);
-    BluetoothGatt bluetoothGatt = ShadowBluetoothGatt.newInstance(bluetoothDevice);
     assertThat(bluetoothGatt).isNotNull();
   }
 
   @Test
   public void canSetAndGetGattCallback() {
-    BluetoothDevice bluetoothDevice = ShadowBluetoothDevice.newInstance(MOCK_MAC_ADDRESS);
-    BluetoothGatt bluetoothGatt = ShadowBluetoothGatt.newInstance(bluetoothDevice);
-    BluetoothGattCallback callback = new BluetoothGattCallback() {};
-
     shadowOf(bluetoothGatt).setGattCallback(callback);
-
     assertThat(shadowOf(bluetoothGatt).getGattCallback()).isEqualTo(callback);
   }
 
-  @Config(minSdk = JELLY_BEAN_MR2)
-  public void connect_returnsTrue() {
-    BluetoothDevice bluetoothDevice = ShadowBluetoothDevice.newInstance(MOCK_MAC_ADDRESS);
-    BluetoothGatt bluetoothGatt = ShadowBluetoothGatt.newInstance(bluetoothDevice);
+  @Test
+  public void isNotConnected_beforeConnect() {
+    assertThat(shadowOf(bluetoothGatt).isConnected()).isFalse();
+    assertThat(resultStatus).isEqualTo(INITIAL_VALUE);
+    assertThat(resultState).isEqualTo(INITIAL_VALUE);
+    assertThat(resultAction).isNull();
+  }
+
+  @Test
+  public void isConnected_returnsFalseWithoutCallback() {
+    assertThat(bluetoothGatt.connect()).isFalse();
+    assertThat(shadowOf(bluetoothGatt).isConnected()).isFalse();
+  }
+
+  @Test
+  public void isConnected_afterConnect() {
+    shadowOf(bluetoothGatt).setGattCallback(callback);
     assertThat(bluetoothGatt.connect()).isTrue();
+    assertThat(shadowOf(bluetoothGatt).isConnected()).isTrue();
+    assertThat(resultStatus).isEqualTo(BluetoothGatt.GATT_SUCCESS);
+    assertThat(resultState).isEqualTo(BluetoothProfile.STATE_CONNECTED);
+    assertThat(resultAction).isEqualTo(ACTION_CONNECTION);
+  }
+
+  @Test
+  public void isConnected_afterConnectAndDisconnect() {
+    shadowOf(bluetoothGatt).setGattCallback(callback);
+    bluetoothGatt.connect();
+    bluetoothGatt.disconnect();
+    assertThat(shadowOf(bluetoothGatt).isConnected()).isFalse();
+    assertThat(resultStatus).isEqualTo(BluetoothGatt.GATT_SUCCESS);
+    assertThat(resultState).isEqualTo(BluetoothProfile.STATE_DISCONNECTED);
+    assertThat(resultAction).isEqualTo(ACTION_CONNECTION);
+  }
+
+  @Test
+  public void isNotConnected_afterOnlyDisconnect() {
+    shadowOf(bluetoothGatt).setGattCallback(callback);
+    bluetoothGatt.disconnect();
+    assertThat(shadowOf(bluetoothGatt).isConnected()).isFalse();
+    assertThat(resultStatus).isEqualTo(INITIAL_VALUE);
+    assertThat(resultState).isEqualTo(INITIAL_VALUE);
+    assertThat(resultAction).isNull();
+  }
+
+  @Test
+  public void isNotConnected_afterConnectAndDisconnectWithoutCallback() {
+    shadowOf(bluetoothGatt).setGattCallback(callback);
+    bluetoothGatt.connect();
+    shadowOf(bluetoothGatt).setGattCallback(null);
+    bluetoothGatt.disconnect();
+    assertThat(shadowOf(bluetoothGatt).isConnected()).isFalse();
+    assertThat(resultStatus).isEqualTo(BluetoothGatt.GATT_SUCCESS);
+    assertThat(resultState).isEqualTo(BluetoothProfile.STATE_CONNECTED);
+  }
+
+  @Test
+  public void isNotClosedbeforeClose() {
+    assertThat(shadowOf(bluetoothGatt).isClosed()).isFalse();
+  }
+
+  @Test
+  public void isClosedafterClose() {
+    bluetoothGatt.close();
+    assertThat(shadowOf(bluetoothGatt).isClosed()).isTrue();
+  }
+
+  @Test
+  public void isDisconnected_afterClose() {
+    shadowOf(bluetoothGatt).setGattCallback(callback);
+    bluetoothGatt.connect();
+    bluetoothGatt.close();
+    assertThat(shadowOf(bluetoothGatt).isConnected()).isFalse();
+  }
+
+  @Test
+  @Config(minSdk = O)
+  public void getConnectionPriority_atInitiation() {
+    assertThat(shadowOf(bluetoothGatt).getConnectionPriority())
+        .isEqualTo(BluetoothGatt.CONNECTION_PRIORITY_BALANCED);
+  }
+
+  @Test
+  @Config(minSdk = O)
+  public void requestConnectionPriority_inRange() {
+    boolean res =
+        bluetoothGatt.requestConnectionPriority(BluetoothGatt.CONNECTION_PRIORITY_LOW_POWER);
+    assertThat(shadowOf(bluetoothGatt).getConnectionPriority())
+        .isEqualTo(BluetoothGatt.CONNECTION_PRIORITY_LOW_POWER);
+    assertThat(res).isTrue();
+    res = bluetoothGatt.requestConnectionPriority(BluetoothGatt.CONNECTION_PRIORITY_BALANCED);
+    assertThat(shadowOf(bluetoothGatt).getConnectionPriority())
+        .isEqualTo(BluetoothGatt.CONNECTION_PRIORITY_BALANCED);
+    assertThat(res).isTrue();
+  }
+
+  @Test
+  @Config(minSdk = O)
+  public void requestConnectionPriority_notInRange_throwsException() {
+    assertThrows(IllegalArgumentException.class, () -> bluetoothGatt.requestConnectionPriority(-9));
+    assertThrows(IllegalArgumentException.class, () -> bluetoothGatt.requestConnectionPriority(9));
+  }
+
+  @Test
+  @Config(minSdk = O)
+  public void discoverServices_noDiscoverableServices_returnsFalse() {
+    assertThat(bluetoothGatt.discoverServices()).isFalse();
+    assertThat(bluetoothGatt.getServices()).isEmpty();
+  }
+
+  @Test
+  @Config(minSdk = O)
+  public void getServices_afterAddService() {
+    shadowOf(bluetoothGatt).addDiscoverableService(service1);
+    assertThat(bluetoothGatt.discoverServices()).isFalse();
+    assertThat(bluetoothGatt.getServices()).hasSize(1);
+  }
+
+  @Test
+  @Config(minSdk = O)
+  public void getServices_afterAddMultipleService() {
+    shadowOf(bluetoothGatt).addDiscoverableService(service1);
+    shadowOf(bluetoothGatt).addDiscoverableService(service2);
+    assertThat(bluetoothGatt.discoverServices()).isFalse();
+    assertThat(bluetoothGatt.getServices()).hasSize(2);
+  }
+
+  @Test
+  @Config(minSdk = O)
+  public void getServices_noDiscoverableServices_withCallback() {
+    shadowOf(bluetoothGatt).setGattCallback(callback);
+    assertThat(bluetoothGatt.discoverServices()).isFalse();
+    assertThat(bluetoothGatt.getServices()).isEmpty();
+    assertThat(resultStatus).isEqualTo(INITIAL_VALUE);
+    assertThat(resultAction).isNull();
+  }
+
+  @Test
+  @Config(minSdk = O)
+  public void getServices_afterAddService_withCallback() {
+    shadowOf(bluetoothGatt).setGattCallback(callback);
+    shadowOf(bluetoothGatt).addDiscoverableService(service1);
+    assertThat(bluetoothGatt.discoverServices()).isTrue();
+    assertThat(bluetoothGatt.getServices()).hasSize(1);
+    assertThat(bluetoothGatt.getServices()).contains(service1);
+    assertThat(resultStatus).isEqualTo(BluetoothGatt.GATT_SUCCESS);
+    assertThat(resultAction).isEqualTo(ACTION_DISCOVER);
+  }
+
+  @Test
+  @Config(minSdk = O)
+  public void getServices_afterAddMultipleService_withCallback() {
+    shadowOf(bluetoothGatt).setGattCallback(callback);
+    shadowOf(bluetoothGatt).addDiscoverableService(service1);
+    shadowOf(bluetoothGatt).addDiscoverableService(service2);
+    assertThat(bluetoothGatt.discoverServices()).isTrue();
+    assertThat(bluetoothGatt.getServices()).hasSize(2);
+    assertThat(bluetoothGatt.getServices()).contains(service1);
+    assertThat(bluetoothGatt.getServices()).contains(service2);
+    assertThat(resultStatus).isEqualTo(BluetoothGatt.GATT_SUCCESS);
+    assertThat(resultAction).isEqualTo(ACTION_DISCOVER);
+  }
+
+  @Test
+  @Config(minSdk = O)
+  public void discoverServices_clearsService() {
+    shadowOf(bluetoothGatt).setGattCallback(callback);
+    shadowOf(bluetoothGatt).addDiscoverableService(service1);
+    shadowOf(bluetoothGatt).removeDiscoverableService(service1);
+    shadowOf(bluetoothGatt).removeDiscoverableService(service2);
+    assertThat(bluetoothGatt.discoverServices()).isFalse();
+    assertThat(bluetoothGatt.getServices()).isEmpty();
+  }
+
+  @Test
+  @Config
+  public void readIncomingCharacteristic_withoutCallback() {
+    assertThrows(
+        IllegalStateException.class,
+        () -> shadowOf(bluetoothGatt).readIncomingCharacteristic(characteristicWithReadProperty));
+  }
+
+  @Test
+  @Config
+  public void readIncomingCharacteristic_withCallback() {
+    shadowOf(bluetoothGatt).setGattCallback(callback);
+    assertThat(shadowOf(bluetoothGatt).readIncomingCharacteristic(characteristicWithReadProperty))
+        .isFalse();
+    assertThat(resultStatus).isEqualTo(INITIAL_VALUE);
+    assertThat(resultAction).isNull();
+    assertThat(resultCharacteristic).isNull();
+    assertThat(shadowOf(bluetoothGatt).getLatestReadBytes()).isNull();
+  }
+
+  @Test
+  @Config
+  public void readIncomingCharacteristic_withCallbackAndServiceSet() {
+    shadowOf(bluetoothGatt).setGattCallback(callback);
+    service1.addCharacteristic(characteristicWithReadProperty);
+    assertThat(characteristicWithReadProperty.getService()).isNotNull();
+    assertThat(shadowOf(bluetoothGatt).readIncomingCharacteristic(characteristicWithReadProperty))
+        .isTrue();
+    assertThat(resultStatus).isEqualTo(BluetoothGatt.GATT_SUCCESS);
+    assertThat(resultAction).isEqualTo(ACTION_READ);
+    assertThat(resultCharacteristic).isEqualTo(characteristicWithReadProperty);
+    assertThat(shadowOf(bluetoothGatt).getLatestReadBytes()).isNull();
+  }
+
+  @Test
+  @Config
+  public void readIncomingCharacteristic_withCallbackAndServiceSet_withValue() {
+    shadowOf(bluetoothGatt).setGattCallback(callback);
+    service1.addCharacteristic(characteristicWithReadProperty);
+    assertThat(characteristicWithReadProperty.getService()).isNotNull();
+    characteristicWithReadProperty.setValue(CHARACTERISTIC_VALUE);
+    assertThat(shadowOf(bluetoothGatt).readIncomingCharacteristic(characteristicWithReadProperty))
+        .isTrue();
+    assertThat(resultStatus).isEqualTo(BluetoothGatt.GATT_SUCCESS);
+    assertThat(resultAction).isEqualTo(ACTION_READ);
+    assertThat(resultCharacteristic).isEqualTo(characteristicWithReadProperty);
+    assertThat(shadowOf(bluetoothGatt).getLatestReadBytes()).isEqualTo(CHARACTERISTIC_VALUE);
+  }
+
+  @Test
+  @Config
+  public void readIncomingCharacteristic_withCallbackAndServiceSet_wrongProperty() {
+    shadowOf(bluetoothGatt).setGattCallback(callback);
+    service1.addCharacteristic(characteristicWithWriteProperties);
+    assertThat(characteristicWithWriteProperties.getService()).isNotNull();
+    assertThat(
+            shadowOf(bluetoothGatt).readIncomingCharacteristic(characteristicWithWriteProperties))
+        .isFalse();
+    assertThat(resultStatus).isEqualTo(INITIAL_VALUE);
+    assertThat(resultAction).isNull();
+    assertThat(resultCharacteristic).isNull();
+    assertThat(shadowOf(bluetoothGatt).getLatestReadBytes()).isNull();
+  }
+
+  @Test
+  @Config
+  public void writeIncomingCharacteristic_withoutCallback() {
+    service1.addCharacteristic(characteristicWithWriteProperties);
+    assertThrows(
+        IllegalStateException.class,
+        () ->
+            shadowOf(bluetoothGatt).writeIncomingCharacteristic(characteristicWithWriteProperties));
+  }
+
+  @Test
+  @Config
+  public void writeIncomingCharacteristic_withCallbackOnly() {
+    shadowOf(bluetoothGatt).setGattCallback(callback);
+    assertThat(
+            shadowOf(bluetoothGatt).writeIncomingCharacteristic(characteristicWithWriteProperties))
+        .isFalse();
+    assertThat(resultStatus).isEqualTo(INITIAL_VALUE);
+    assertThat(resultAction).isNull();
+    assertThat(resultCharacteristic).isNull();
+    assertThat(shadowOf(bluetoothGatt).getLatestWrittenBytes()).isNull();
+  }
+
+  @Test
+  @Config
+  public void writeIncomingCharacteristic_withCallbackAndServiceSet() {
+    shadowOf(bluetoothGatt).setGattCallback(callback);
+    service2.addCharacteristic(characteristicWithWriteProperties);
+    assertThat(characteristicWithWriteProperties.getService()).isNotNull();
+    assertThat(
+            shadowOf(bluetoothGatt).writeIncomingCharacteristic(characteristicWithWriteProperties))
+        .isTrue();
+    assertThat(resultStatus).isEqualTo(BluetoothGatt.GATT_SUCCESS);
+    assertThat(resultAction).isEqualTo(ACTION_WRITE);
+    assertThat(resultCharacteristic).isEqualTo(characteristicWithWriteProperties);
+    assertThat(shadowOf(bluetoothGatt).getLatestWrittenBytes()).isNull();
+  }
+
+  @Test
+  @Config
+  public void writeIncomingCharacteristic_withCallbackAndServiceSet_wrongProperty() {
+    shadowOf(bluetoothGatt).setGattCallback(callback);
+    service1.addCharacteristic(characteristicWithReadProperty);
+    assertThat(characteristicWithReadProperty.getService()).isNotNull();
+    assertThat(shadowOf(bluetoothGatt).writeIncomingCharacteristic(characteristicWithReadProperty))
+        .isFalse();
+    assertThat(resultStatus).isEqualTo(INITIAL_VALUE);
+    assertThat(resultAction).isNull();
+    assertThat(resultCharacteristic).isNull();
+    assertThat(shadowOf(bluetoothGatt).getLatestWrittenBytes()).isNull();
+    assertThat(shadowOf(bluetoothGatt).getLatestWrittenBytes()).isNull();
+  }
+
+  @Test
+  @Config
+  public void writeIncomingCharacteristic_correctlySetup_noValue() {
+    shadowOf(bluetoothGatt).setGattCallback(callback);
+    service1.addCharacteristic(characteristicWithWriteProperties);
+    assertThat(characteristicWithWriteProperties.getService()).isNotNull();
+    assertThat(
+            shadowOf(bluetoothGatt).writeIncomingCharacteristic(characteristicWithWriteProperties))
+        .isTrue();
+    assertThat(resultStatus).isEqualTo(BluetoothGatt.GATT_SUCCESS);
+    assertThat(resultAction).isEqualTo(ACTION_WRITE);
+    assertThat(resultCharacteristic).isEqualTo(characteristicWithWriteProperties);
+    assertThat(shadowOf(bluetoothGatt).getLatestWrittenBytes()).isNull();
+  }
+
+  @Test
+  @Config
+  public void writeIncomingCharacteristic_correctlySetup_withValue() {
+    shadowOf(bluetoothGatt).setGattCallback(callback);
+    service1.addCharacteristic(characteristicWithWriteProperties);
+    characteristicWithWriteProperties.setValue(CHARACTERISTIC_VALUE);
+    assertThat(characteristicWithWriteProperties.getService()).isNotNull();
+    assertThat(
+            shadowOf(bluetoothGatt).writeIncomingCharacteristic(characteristicWithWriteProperties))
+        .isTrue();
+    assertThat(resultStatus).isEqualTo(BluetoothGatt.GATT_SUCCESS);
+    assertThat(resultAction).isEqualTo(ACTION_WRITE);
+    assertThat(resultCharacteristic).isEqualTo(characteristicWithWriteProperties);
+
+    assertThat(shadowOf(bluetoothGatt).getLatestWrittenBytes()).isEqualTo(CHARACTERISTIC_VALUE);
+  }
+
+  @Test
+  @Config
+  public void writeIncomingCharacteristic_correctlySetup_onlyWriteProperty() {
+
+    BluetoothGattCharacteristic characteristic =
+        new BluetoothGattCharacteristic(
+            UUID.fromString("00000000-0000-0000-0000-0000000000A6"),
+            BluetoothGattCharacteristic.PROPERTY_WRITE,
+            BluetoothGattCharacteristic.PERMISSION_WRITE);
+
+    shadowOf(bluetoothGatt).setGattCallback(callback);
+    service1.addCharacteristic(characteristic);
+    characteristic.setValue(CHARACTERISTIC_VALUE);
+    assertThat(shadowOf(bluetoothGatt).writeIncomingCharacteristic(characteristic)).isTrue();
+    assertThat(resultStatus).isEqualTo(BluetoothGatt.GATT_SUCCESS);
+    assertThat(resultAction).isEqualTo(ACTION_WRITE);
+    assertThat(resultCharacteristic).isEqualTo(characteristic);
+    assertThat(shadowOf(bluetoothGatt).getLatestWrittenBytes()).isEqualTo(CHARACTERISTIC_VALUE);
+  }
+
+  @Test
+  @Config
+  public void writeIncomingCharacteristic_correctlySetup_onlyWriteNoResponseProperty() {
+
+    BluetoothGattCharacteristic characteristic =
+        new BluetoothGattCharacteristic(
+            UUID.fromString("00000000-0000-0000-0000-0000000000A7"),
+            BluetoothGattCharacteristic.PROPERTY_WRITE_NO_RESPONSE,
+            BluetoothGattCharacteristic.PERMISSION_WRITE);
+
+    shadowOf(bluetoothGatt).setGattCallback(callback);
+    service1.addCharacteristic(characteristic);
+    characteristic.setValue(CHARACTERISTIC_VALUE);
+    assertThat(shadowOf(bluetoothGatt).writeIncomingCharacteristic(characteristic)).isTrue();
+    assertThat(resultStatus).isEqualTo(BluetoothGatt.GATT_SUCCESS);
+    assertThat(resultAction).isEqualTo(ACTION_WRITE);
+    assertThat(resultCharacteristic).isEqualTo(characteristic);
+    assertThat(shadowOf(bluetoothGatt).getLatestWrittenBytes()).isEqualTo(CHARACTERISTIC_VALUE);
   }
 }
diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowBluetoothHeadsetTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowBluetoothHeadsetTest.java
index 9a13954..9482ba8 100644
--- a/robolectric/src/test/java/org/robolectric/shadows/ShadowBluetoothHeadsetTest.java
+++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowBluetoothHeadsetTest.java
@@ -2,6 +2,7 @@
 
 import static android.os.Build.VERSION_CODES.KITKAT;
 import static android.os.Build.VERSION_CODES.P;
+import static android.os.Build.VERSION_CODES.S;
 import static android.os.Looper.getMainLooper;
 import static com.google.common.truth.Truth.assertThat;
 import static org.junit.Assert.fail;
@@ -174,6 +175,20 @@
   }
 
   @Test
+  @Config(minSdk = S)
+  public void isVoiceRecognitionSupported_supportedByDefault() {
+    assertThat(bluetoothHeadset.isVoiceRecognitionSupported(device1)).isTrue();
+  }
+
+  @Test
+  @Config(minSdk = S)
+  public void setVoiceRecognitionSupported_false_notSupported() {
+    shadowOf(bluetoothHeadset).setVoiceRecognitionSupported(false);
+
+    assertThat(bluetoothHeadset.isVoiceRecognitionSupported(device1)).isFalse();
+  }
+
+  @Test
   @Config(minSdk = KITKAT)
   public void sendVendorSpecificResultCode_defaultsToTrueForConnectedDevice() {
     shadowOf(bluetoothHeadset).addConnectedDevice(device1);
diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowDevicePolicyManagerTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowDevicePolicyManagerTest.java
index 09a3b54..7d36f35 100644
--- a/robolectric/src/test/java/org/robolectric/shadows/ShadowDevicePolicyManagerTest.java
+++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowDevicePolicyManagerTest.java
@@ -9,6 +9,9 @@
 import static android.app.admin.DevicePolicyManager.ENCRYPTION_STATUS_ACTIVE_PER_USER;
 import static android.app.admin.DevicePolicyManager.ENCRYPTION_STATUS_INACTIVE;
 import static android.app.admin.DevicePolicyManager.ENCRYPTION_STATUS_UNSUPPORTED;
+import static android.app.admin.DevicePolicyManager.LOCK_TASK_FEATURE_HOME;
+import static android.app.admin.DevicePolicyManager.LOCK_TASK_FEATURE_NOTIFICATIONS;
+import static android.app.admin.DevicePolicyManager.LOCK_TASK_FEATURE_OVERVIEW;
 import static android.app.admin.DevicePolicyManager.PASSWORD_COMPLEXITY_HIGH;
 import static android.app.admin.DevicePolicyManager.PERMISSION_POLICY_AUTO_GRANT;
 import static android.app.admin.DevicePolicyManager.STATE_USER_SETUP_COMPLETE;
@@ -1698,6 +1701,91 @@
   }
 
   @Test
+  @Config(minSdk = P)
+  public void getLockTaskFeatures_nullAdmin_throwsNullPointerException() {
+    shadowOf(devicePolicyManager).setProfileOwner(testComponent);
+    assertThrows(NullPointerException.class, () -> devicePolicyManager.getLockTaskFeatures(null));
+  }
+
+  @Test
+  @Config(minSdk = P)
+  public void getLockTaskFeatures_notOwner_throwsSecurityException() {
+    assertThrows(
+        SecurityException.class, () -> devicePolicyManager.getLockTaskFeatures(testComponent));
+  }
+
+  @Test
+  @Config(minSdk = P)
+  public void getLockTaskFeatures_default_noFeatures() {
+    shadowOf(devicePolicyManager).setProfileOwner(testComponent);
+
+    assertThat(devicePolicyManager.getLockTaskFeatures(testComponent)).isEqualTo(0);
+  }
+
+  @Test
+  @Config(minSdk = P)
+  public void setLockTaskFeatures_nullAdmin_throwsNullPointerException() {
+    shadowOf(devicePolicyManager).setProfileOwner(testComponent);
+
+    assertThrows(
+        NullPointerException.class, () -> devicePolicyManager.setLockTaskFeatures(null, 0));
+  }
+
+  @Test
+  @Config(minSdk = P)
+  public void setLockTaskFeatures_notOwner_throwsSecurityException() {
+    assertThrows(
+        SecurityException.class, () -> devicePolicyManager.setLockTaskFeatures(testComponent, 0));
+  }
+
+  @Test
+  @Config(minSdk = P)
+  public void setLockTaskFeatures_overviewWithoutHome_throwsIllegalArgumentException() {
+    shadowOf(devicePolicyManager).setProfileOwner(testComponent);
+
+    assertThrows(
+        IllegalArgumentException.class,
+        () -> devicePolicyManager.setLockTaskFeatures(testComponent, LOCK_TASK_FEATURE_OVERVIEW));
+  }
+
+  @Test
+  @Config(minSdk = P)
+  public void setLockTaskFeatures_notificationsWithoutHome_throwsIllegalArgumentException() {
+    shadowOf(devicePolicyManager).setProfileOwner(testComponent);
+
+    assertThrows(
+        IllegalArgumentException.class,
+        () ->
+            devicePolicyManager.setLockTaskFeatures(
+                testComponent, LOCK_TASK_FEATURE_NOTIFICATIONS));
+  }
+
+  @Test
+  @Config(minSdk = P)
+  public void setLockTaskFeatures_homeOverviewNotifications_success() {
+    shadowOf(devicePolicyManager).setProfileOwner(testComponent);
+
+    int flags =
+        LOCK_TASK_FEATURE_HOME | LOCK_TASK_FEATURE_OVERVIEW | LOCK_TASK_FEATURE_NOTIFICATIONS;
+    devicePolicyManager.setLockTaskFeatures(testComponent, flags);
+
+    assertThat(devicePolicyManager.getLockTaskFeatures(testComponent)).isEqualTo(flags);
+  }
+
+  @Test
+  @Config(minSdk = P)
+  public void setLockTaskFeatures_setFeaturesTwice_keepsLatestFeatures() {
+    shadowOf(devicePolicyManager).setProfileOwner(testComponent);
+    devicePolicyManager.setLockTaskFeatures(testComponent, LOCK_TASK_FEATURE_HOME);
+
+    int flags =
+        LOCK_TASK_FEATURE_HOME | LOCK_TASK_FEATURE_OVERVIEW | LOCK_TASK_FEATURE_NOTIFICATIONS;
+    devicePolicyManager.setLockTaskFeatures(testComponent, flags);
+
+    assertThat(devicePolicyManager.getLockTaskFeatures(testComponent)).isEqualTo(flags);
+  }
+
+  @Test
   @Config(minSdk = LOLLIPOP)
   public void getLockTaskPackages_notOwner() {
     try {
diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowImsMmTelManagerTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowImsMmTelManagerTest.java
index d31d185..f019621 100644
--- a/robolectric/src/test/java/org/robolectric/shadows/ShadowImsMmTelManagerTest.java
+++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowImsMmTelManagerTest.java
@@ -11,10 +11,12 @@
 import android.telephony.ims.ImsException;
 import android.telephony.ims.ImsMmTelManager;
 import android.telephony.ims.ImsMmTelManager.CapabilityCallback;
-import android.telephony.ims.ImsMmTelManager.RegistrationCallback;
 import android.telephony.ims.ImsReasonInfo;
+import android.telephony.ims.ImsRegistrationAttributes;
+import android.telephony.ims.RegistrationManager;
 import android.telephony.ims.feature.MmTelFeature.MmTelCapabilities;
 import android.telephony.ims.stub.ImsRegistrationImplBase;
+import android.util.ArraySet;
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -34,24 +36,54 @@
   }
 
   @Test
-  public void registerImsRegistrationCallback_imsRegistering_onRegisteringInvoked()
+  public void registerImsRegistrationManagerCallback_imsRegistering_onRegisteringInvoked()
       throws ImsException {
-    RegistrationCallback registrationCallback = mock(RegistrationCallback.class);
+    RegistrationManager.RegistrationCallback registrationCallback =
+        mock(RegistrationManager.RegistrationCallback.class);
+
     shadowImsMmTelManager.registerImsRegistrationCallback(Runnable::run, registrationCallback);
     shadowImsMmTelManager.setImsRegistering(ImsRegistrationImplBase.REGISTRATION_TECH_LTE);
 
     verify(registrationCallback).onRegistering(ImsRegistrationImplBase.REGISTRATION_TECH_LTE);
 
     shadowImsMmTelManager.unregisterImsRegistrationCallback(registrationCallback);
-    shadowImsMmTelManager.setImsRegistering(ImsRegistrationImplBase.REGISTRATION_TECH_IWLAN);
+    shadowImsMmTelManager.setImsRegistering(ImsRegistrationImplBase.REGISTRATION_TECH_LTE);
 
     verifyNoMoreInteractions(registrationCallback);
   }
 
   @Test
-  public void registerImsRegistrationCallback_imsRegistered_onRegisteredInvoked()
+  @Config(sdk = {VERSION_CODES.S, Config.NEWEST_SDK})
+  public void registerImsRegistrationManagerCallbackImsAttrs_imsRegistering_onRegisteringInvoked()
       throws ImsException {
-    RegistrationCallback registrationCallback = mock(RegistrationCallback.class);
+    RegistrationManager.RegistrationCallback registrationCallback =
+        mock(RegistrationManager.RegistrationCallback.class);
+
+    int imsRegistrationTech = ImsRegistrationImplBase.REGISTRATION_TECH_IWLAN;
+    int imsTransportType = RegistrationManager.getAccessType(imsRegistrationTech);
+    int imsAttributeFlags = 0;
+    ArraySet<String> featureTags = new ArraySet<>();
+
+    ImsRegistrationAttributes imsRegistrationAttrs =
+        new ImsRegistrationAttributes(
+            imsRegistrationTech, imsTransportType, imsAttributeFlags, featureTags);
+
+    shadowImsMmTelManager.registerImsRegistrationCallback(Runnable::run, registrationCallback);
+    shadowImsMmTelManager.setImsRegistering(imsRegistrationAttrs);
+
+    verify(registrationCallback).onRegistering(imsRegistrationAttrs);
+
+    shadowImsMmTelManager.unregisterImsRegistrationCallback(registrationCallback);
+    shadowImsMmTelManager.setImsRegistering(imsRegistrationAttrs);
+
+    verifyNoMoreInteractions(registrationCallback);
+  }
+
+  @Test
+  public void registerImsRegistrationManagerCallback_imsRegistered_onRegisteredInvoked()
+      throws ImsException {
+    RegistrationManager.RegistrationCallback registrationCallback =
+        mock(RegistrationManager.RegistrationCallback.class);
     shadowImsMmTelManager.registerImsRegistrationCallback(Runnable::run, registrationCallback);
     shadowImsMmTelManager.setImsRegistered(ImsRegistrationImplBase.REGISTRATION_TECH_IWLAN);
 
@@ -64,9 +96,37 @@
   }
 
   @Test
-  public void registerImsRegistrationCallback_imsUnregistered_onUnregisteredInvoked()
+  @Config(sdk = {VERSION_CODES.S, Config.NEWEST_SDK})
+  public void registerImsRegistrationManagerCallbackImsAttrs_imsRegistered_onRegisteredInvoked()
       throws ImsException {
-    RegistrationCallback registrationCallback = mock(RegistrationCallback.class);
+    RegistrationManager.RegistrationCallback registrationCallback =
+        mock(RegistrationManager.RegistrationCallback.class);
+
+    int imsRegistrationTech = ImsRegistrationImplBase.REGISTRATION_TECH_IWLAN;
+    int imsTransportType = RegistrationManager.getAccessType(imsRegistrationTech);
+    int imsAttributeFlags = 0;
+    ArraySet<String> featureTags = new ArraySet<>();
+
+    ImsRegistrationAttributes imsRegistrationAttrs =
+        new ImsRegistrationAttributes(
+            imsRegistrationTech, imsTransportType, imsAttributeFlags, featureTags);
+
+    shadowImsMmTelManager.registerImsRegistrationCallback(Runnable::run, registrationCallback);
+    shadowImsMmTelManager.setImsRegistered(imsRegistrationAttrs);
+
+    verify(registrationCallback).onRegistered(imsRegistrationAttrs);
+
+    shadowImsMmTelManager.unregisterImsRegistrationCallback(registrationCallback);
+    shadowImsMmTelManager.setImsRegistered(imsRegistrationAttrs);
+
+    verifyNoMoreInteractions(registrationCallback);
+  }
+
+  @Test
+  public void registerImsRegistrationManagerCallback_imsDeregistered_onDeregisteredInvoked()
+      throws ImsException {
+    RegistrationManager.RegistrationCallback registrationCallback =
+        mock(RegistrationManager.RegistrationCallback.class);
     shadowImsMmTelManager.registerImsRegistrationCallback(Runnable::run, registrationCallback);
     ImsReasonInfo imsReasonInfoWithCallbackRegistered = new ImsReasonInfo();
     shadowImsMmTelManager.setImsUnregistered(imsReasonInfoWithCallbackRegistered);
@@ -81,11 +141,98 @@
   }
 
   @Test
-  public void registerImsRegistrationCallback_imsNotSupported_imsExceptionThrown() {
+  public void
+      registerImsRegistrationManagerCallback_imsTechnologyChangeFailed_onTechnologyChangeFailedInvoked()
+          throws ImsException {
+    RegistrationManager.RegistrationCallback registrationCallback =
+        mock(RegistrationManager.RegistrationCallback.class);
+    shadowImsMmTelManager.registerImsRegistrationCallback(Runnable::run, registrationCallback);
+    ImsReasonInfo imsReasonInfoWithCallbackRegistered = new ImsReasonInfo();
+    shadowImsMmTelManager.setOnTechnologyChangeFailed(
+        ImsRegistrationImplBase.REGISTRATION_TECH_IWLAN, imsReasonInfoWithCallbackRegistered);
+
+    verify(registrationCallback)
+        .onTechnologyChangeFailed(
+            ImsRegistrationImplBase.REGISTRATION_TECH_IWLAN, imsReasonInfoWithCallbackRegistered);
+
+    ImsReasonInfo imsReasonInfoAfterUnregisteringCallback = new ImsReasonInfo();
+    shadowImsMmTelManager.unregisterImsRegistrationCallback(registrationCallback);
+    shadowImsMmTelManager.setOnTechnologyChangeFailed(
+        ImsRegistrationImplBase.REGISTRATION_TECH_IWLAN, imsReasonInfoAfterUnregisteringCallback);
+
+    verifyNoMoreInteractions(registrationCallback);
+  }
+
+  @Test
+  public void
+      registerImsMmTelManagerRegistrationManagerCallback_imsNotSupported_imsExceptionThrown() {
     shadowImsMmTelManager.setImsAvailableOnDevice(false);
     try {
       shadowImsMmTelManager.registerImsRegistrationCallback(
-          Runnable::run, mock(RegistrationCallback.class));
+          Runnable::run, mock(RegistrationManager.RegistrationCallback.class));
+      assertWithMessage("Expected ImsException was not thrown").fail();
+    } catch (ImsException e) {
+      assertThat(e.getCode()).isEqualTo(ImsException.CODE_ERROR_UNSUPPORTED_OPERATION);
+      assertThat(e).hasMessageThat().contains("IMS not available on device.");
+    }
+  }
+
+  @Test
+  public void registerImsMmTelManagerRegistrationCallback_imsRegistering_onRegisteringInvoked()
+      throws ImsException {
+    ImsMmTelManager.RegistrationCallback registrationCallback =
+        mock(ImsMmTelManager.RegistrationCallback.class);
+    shadowImsMmTelManager.registerImsRegistrationCallback(Runnable::run, registrationCallback);
+    shadowImsMmTelManager.setImsRegistering(ImsRegistrationImplBase.REGISTRATION_TECH_LTE);
+
+    verify(registrationCallback).onRegistering(ImsRegistrationImplBase.REGISTRATION_TECH_LTE);
+
+    shadowImsMmTelManager.unregisterImsRegistrationCallback(registrationCallback);
+    shadowImsMmTelManager.setImsRegistering(ImsRegistrationImplBase.REGISTRATION_TECH_IWLAN);
+
+    verifyNoMoreInteractions(registrationCallback);
+  }
+
+  @Test
+  public void registerImsMmTelManagerRegistrationCallback_imsRegistered_onRegisteredInvoked()
+      throws ImsException {
+    ImsMmTelManager.RegistrationCallback registrationCallback =
+        mock(ImsMmTelManager.RegistrationCallback.class);
+    shadowImsMmTelManager.registerImsRegistrationCallback(Runnable::run, registrationCallback);
+    shadowImsMmTelManager.setImsRegistered(ImsRegistrationImplBase.REGISTRATION_TECH_IWLAN);
+
+    verify(registrationCallback).onRegistered(ImsRegistrationImplBase.REGISTRATION_TECH_IWLAN);
+
+    shadowImsMmTelManager.unregisterImsRegistrationCallback(registrationCallback);
+    shadowImsMmTelManager.setImsRegistered(ImsRegistrationImplBase.REGISTRATION_TECH_LTE);
+
+    verifyNoMoreInteractions(registrationCallback);
+  }
+
+  @Test
+  public void registerImsMmTelManagerRegistrationCallback_imsUnregistered_onUnregisteredInvoked()
+      throws ImsException {
+    ImsMmTelManager.RegistrationCallback registrationCallback =
+        mock(ImsMmTelManager.RegistrationCallback.class);
+    shadowImsMmTelManager.registerImsRegistrationCallback(Runnable::run, registrationCallback);
+    ImsReasonInfo imsReasonInfoWithCallbackRegistered = new ImsReasonInfo();
+    shadowImsMmTelManager.setImsUnregistered(imsReasonInfoWithCallbackRegistered);
+
+    verify(registrationCallback).onUnregistered(imsReasonInfoWithCallbackRegistered);
+
+    ImsReasonInfo imsReasonInfoAfterUnregisteringCallback = new ImsReasonInfo();
+    shadowImsMmTelManager.unregisterImsRegistrationCallback(registrationCallback);
+    shadowImsMmTelManager.setImsUnregistered(imsReasonInfoAfterUnregisteringCallback);
+
+    verifyNoMoreInteractions(registrationCallback);
+  }
+
+  @Test
+  public void registerImsMmTelManagerRegistrationCallback_imsNotSupported_imsExceptionThrown() {
+    shadowImsMmTelManager.setImsAvailableOnDevice(false);
+    try {
+      shadowImsMmTelManager.registerImsRegistrationCallback(
+          Runnable::run, mock(ImsMmTelManager.RegistrationCallback.class));
       assertWithMessage("Expected ImsException was not thrown").fail();
     } catch (ImsException e) {
       assertThat(e.getCode()).isEqualTo(ImsException.CODE_ERROR_UNSUPPORTED_OPERATION);
@@ -98,7 +245,8 @@
       registerMmTelCapabilityCallback_imsRegistered_availabilityChange_onCapabilitiesStatusChangedInvoked()
           throws ImsException {
     MmTelCapabilities[] mmTelCapabilities = new MmTelCapabilities[1];
-    CapabilityCallback capabilityCallback = new CapabilityCallback() {
+    CapabilityCallback capabilityCallback =
+        new CapabilityCallback() {
           @Override
           public void onCapabilitiesStatusChanged(MmTelCapabilities capabilities) {
             super.onCapabilitiesStatusChanged(capabilities);
@@ -129,7 +277,8 @@
       registerMmTelCapabilityCallback_imsNotRegistered_availabilityChange_onCapabilitiesStatusChangedNotInvoked()
           throws ImsException {
     MmTelCapabilities[] mmTelCapabilities = new MmTelCapabilities[1];
-    CapabilityCallback capabilityCallback = new CapabilityCallback() {
+    CapabilityCallback capabilityCallback =
+        new CapabilityCallback() {
           @Override
           public void onCapabilitiesStatusChanged(MmTelCapabilities capabilities) {
             super.onCapabilitiesStatusChanged(capabilities);
diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowInsetsControllerTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowInsetsControllerTest.java
new file mode 100644
index 0000000..45c4284
--- /dev/null
+++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowInsetsControllerTest.java
@@ -0,0 +1,72 @@
+package org.robolectric.shadows;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.app.Activity;
+import android.os.Build;
+import android.view.WindowInsets;
+import android.view.WindowInsetsController;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.Robolectric;
+import org.robolectric.android.controller.ActivityController;
+import org.robolectric.annotation.Config;
+
+@RunWith(AndroidJUnit4.class)
+@Config(minSdk = Build.VERSION_CODES.R)
+public class ShadowInsetsControllerTest {
+  private ActivityController<Activity> activityController;
+  private Activity activity;
+  private WindowInsetsController controller;
+
+  @Before
+  public void setUp() {
+    activityController = Robolectric.buildActivity(Activity.class);
+    activityController.setup();
+
+    activity = activityController.get();
+    controller = activity.getWindow().getInsetsController();
+  }
+
+  @Test
+  public void statusBar_show_hide_trackedByWindowInsets() {
+    // Responds to hide.
+    controller.hide(WindowInsets.Type.statusBars());
+    assertStatusBarVisibility(/* isVisible= */ false);
+
+    // Responds to show.
+    controller.show(WindowInsets.Type.statusBars());
+    assertStatusBarVisibility(/* isVisible= */ true);
+
+    // Does not respond to different type.
+    controller.hide(WindowInsets.Type.navigationBars());
+    assertStatusBarVisibility(/* isVisible= */ true);
+  }
+
+  @Test
+  public void navigationBar_show_hide_trackedByWindowInsets() {
+    // Responds to hide.
+    controller.hide(WindowInsets.Type.navigationBars());
+    assertNavigationBarVisibility(/* isVisible= */ false);
+
+    // Responds to show.
+    controller.show(WindowInsets.Type.navigationBars());
+    assertNavigationBarVisibility(/* isVisible= */ true);
+
+    // Does not respond to different type.
+    controller.hide(WindowInsets.Type.statusBars());
+    assertNavigationBarVisibility(/* isVisible= */ true);
+  }
+
+  private void assertStatusBarVisibility(boolean isVisible) {
+    WindowInsets insets = activity.getWindow().getDecorView().getRootWindowInsets();
+    assertThat(insets.isVisible(WindowInsets.Type.statusBars())).isEqualTo(isVisible);
+  }
+
+  private void assertNavigationBarVisibility(boolean isVisible) {
+    WindowInsets insets = activity.getWindow().getDecorView().getRootWindowInsets();
+    assertThat(insets.isVisible(WindowInsets.Type.navigationBars())).isEqualTo(isVisible);
+  }
+}
diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowLayoutAnimationControllerTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowLayoutAnimationControllerTest.java
deleted file mode 100644
index 75a1c8d..0000000
--- a/robolectric/src/test/java/org/robolectric/shadows/ShadowLayoutAnimationControllerTest.java
+++ /dev/null
@@ -1,31 +0,0 @@
-package org.robolectric.shadows;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import android.view.animation.LayoutAnimationController;
-import androidx.test.core.app.ApplicationProvider;
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.robolectric.Shadows;
-
-@RunWith(AndroidJUnit4.class)
-public class ShadowLayoutAnimationControllerTest {
-  private ShadowLayoutAnimationController shadow;
-
-  @Before
-  public void setup() {
-    LayoutAnimationController controller =
-        new LayoutAnimationController(ApplicationProvider.getApplicationContext(), null);
-    shadow = Shadows.shadowOf(controller);
-  }
-
-  @Test
-  public void testResourceId() {
-    int id = 1;
-    shadow.setLoadedFromResourceId(1);
-    assertThat(shadow.getLoadedFromResourceId()).isEqualTo(id);
-  }
-
-}
diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowMatrixTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowMatrixTest.java
index 8ee1f6f..bd99b4c 100644
--- a/robolectric/src/test/java/org/robolectric/shadows/ShadowMatrixTest.java
+++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowMatrixTest.java
@@ -2,7 +2,6 @@
 
 import static android.os.Build.VERSION_CODES.LOLLIPOP;
 import static com.google.common.truth.Truth.assertThat;
-import static org.robolectric.Shadows.shadowOf;
 
 import android.graphics.Matrix;
 import android.graphics.PointF;
@@ -11,6 +10,7 @@
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.robolectric.annotation.Config;
+import org.robolectric.shadow.api.Shadow;
 
 @RunWith(AndroidJUnit4.class)
 public class ShadowMatrixTest {
@@ -23,7 +23,7 @@
     m.preTranslate(16, 23);
     m.preSkew(42, 108);
 
-    assertThat(shadowOf(m).getPreOperations())
+    assertThat(((ShadowMatrix) Shadow.extract(m)).getPreOperations())
         .containsExactly("skew 42.0 108.0", "translate 16.0 23.0", "rotate 4.0 8.0 15.0");
   }
 
@@ -34,7 +34,7 @@
     m.postTranslate(16, 23);
     m.postSkew(42, 108);
 
-    assertThat(shadowOf(m).getPostOperations())
+    assertThat(((ShadowMatrix) Shadow.extract(m)).getPostOperations())
         .containsExactly("rotate 4.0 8.0 15.0", "translate 16.0 23.0", "skew 42.0 108.0");
   }
 
@@ -49,7 +49,8 @@
     m.setRotate(42);
     m.setRotate(108);
 
-    assertThat(shadowOf(m).getSetOperations()).containsEntry("rotate", "108.0");
+    assertThat(((ShadowMatrix) Shadow.extract(m)).getSetOperations())
+        .containsEntry("rotate", "108.0");
   }
 
   @Test
@@ -59,7 +60,7 @@
     matrix.preScale(2, 2, 2, 2);
     matrix.postScale(3, 3, 3, 3);
 
-    final ShadowMatrix shadow = shadowOf(matrix);
+    final ShadowMatrix shadow = Shadow.extract(matrix);
     assertThat(shadow.getSetOperations().get("scale")).isEqualTo("1.0 1.0");
     assertThat(shadow.getPreOperations().get(0)).isEqualTo("scale 2.0 2.0 2.0 2.0");
     assertThat(shadow.getPostOperations().get(0)).isEqualTo("scale 3.0 3.0 3.0 3.0");
@@ -70,7 +71,7 @@
     final Matrix matrix = new Matrix();
     matrix.setScale(1, 2, 3, 4);
 
-    final ShadowMatrix shadow = shadowOf(matrix);
+    final ShadowMatrix shadow = Shadow.extract(matrix);
     assertThat(shadow.getSetOperations().get("scale")).isEqualTo("1.0 2.0 3.0 4.0");
   }
 
@@ -83,7 +84,7 @@
     matrix2.setScale(3, 4);
     matrix2.set(matrix1);
 
-    final ShadowMatrix shadow = shadowOf(matrix2);
+    final ShadowMatrix shadow = Shadow.extract(matrix2);
     assertThat(shadow.getSetOperations().get("scale")).isEqualTo("1.0 2.0");
   }
 
@@ -96,7 +97,7 @@
     matrix2.set(matrix1);
     matrix2.set(null);
 
-    final ShadowMatrix shadow = shadowOf(matrix2);
+    final ShadowMatrix shadow = Shadow.extract(matrix2);
     assertThat(shadow.getSetOperations()).isEmpty();
   }
 
diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowMediaControllerTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowMediaControllerTest.java
index 2bb0dbf..2299246 100644
--- a/robolectric/src/test/java/org/robolectric/shadows/ShadowMediaControllerTest.java
+++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowMediaControllerTest.java
@@ -21,6 +21,7 @@
 import android.media.session.MediaController.PlaybackInfo;
 import android.media.session.MediaSession;
 import android.media.session.PlaybackState;
+import android.os.Bundle;
 import androidx.test.core.app.ApplicationProvider;
 import androidx.test.ext.junit.runners.AndroidJUnit4;
 import java.util.ArrayList;
@@ -134,6 +135,16 @@
 
   @Test
   @Config(minSdk = LOLLIPOP)
+  public void setAndGetExtras() {
+    String extraKey = "test.extra.key";
+    Bundle extras = new Bundle();
+    extras.putBoolean(extraKey, true);
+    shadowMediaController.setExtras(extras);
+    assertEquals(true, mediaController.getExtras().getBoolean(extraKey, false));
+  }
+
+  @Test
+  @Config(minSdk = LOLLIPOP)
   public void registerAndGetCallback() {
     List<MediaController.Callback> mockCallbacks = new ArrayList<>();
     assertEquals(mockCallbacks, shadowMediaController.getCallbacks());
@@ -151,6 +162,23 @@
 
   @Test
   @Config(minSdk = LOLLIPOP)
+  public void registerWithHandlerAndGetCallback() {
+    List<MediaController.Callback> mockCallbacks = new ArrayList<>();
+    assertEquals(mockCallbacks, shadowMediaController.getCallbacks());
+
+    MediaController.Callback mockCallback1 = mock(MediaController.Callback.class);
+    mockCallbacks.add(mockCallback1);
+    mediaController.registerCallback(mockCallback1, null);
+    assertEquals(mockCallbacks, shadowMediaController.getCallbacks());
+
+    MediaController.Callback mockCallback2 = mock(MediaController.Callback.class);
+    mockCallbacks.add(mockCallback2);
+    mediaController.registerCallback(mockCallback2, null);
+    assertEquals(mockCallbacks, shadowMediaController.getCallbacks());
+  }
+
+  @Test
+  @Config(minSdk = LOLLIPOP)
   public void unregisterCallback() {
     List<MediaController.Callback> mockCallbacks = new ArrayList<>();
     MediaController.Callback mockCallback1 = mock(MediaController.Callback.class);
diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowNetworkCapabilitiesTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowNetworkCapabilitiesTest.java
index cb46834..727fce4 100644
--- a/robolectric/src/test/java/org/robolectric/shadows/ShadowNetworkCapabilitiesTest.java
+++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowNetworkCapabilitiesTest.java
@@ -107,4 +107,11 @@
     assertThat(wifiInfo.getSSID()).isEqualTo(String.format("\"%s\"", fakeSsid));
     assertThat(wifiInfo.getBSSID()).isEqualTo(fakeBssid);
   }
+
+  @Test
+  public void setLinkDownstreamBandwidthKbps() {
+    NetworkCapabilities networkCapabilities = ShadowNetworkCapabilities.newInstance();
+    shadowOf(networkCapabilities).setLinkDownstreamBandwidthKbps(100);
+    assertThat(networkCapabilities.getLinkDownstreamBandwidthKbps()).isEqualTo(100);
+  }
 }
diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowPackageManagerTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowPackageManagerTest.java
index 870bb1d..e3242fa 100644
--- a/robolectric/src/test/java/org/robolectric/shadows/ShadowPackageManagerTest.java
+++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowPackageManagerTest.java
@@ -46,6 +46,7 @@
 import static com.google.common.truth.TruthJUnit.assume;
 import static java.util.concurrent.TimeUnit.SECONDS;
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertThrows;
 import static org.junit.Assert.fail;
 import static org.mockito.Mockito.eq;
 import static org.mockito.Mockito.mock;
@@ -76,6 +77,7 @@
 import android.content.pm.PackageInfo;
 import android.content.pm.PackageInstaller;
 import android.content.pm.PackageManager;
+import android.content.pm.PackageManager.ApplicationInfoFlags;
 import android.content.pm.PackageManager.NameNotFoundException;
 import android.content.pm.PackageManager.OnPermissionsChangedListener;
 import android.content.pm.PackageManager.PackageInfoFlags;
@@ -2038,6 +2040,55 @@
   }
 
   @Test
+  @Config(minSdk = TIRAMISU)
+  public void getPackageInfoAfterT_shouldReturnRequestedPermissions() throws Exception {
+    PackageInfo packageInfo =
+        packageManager.getPackageInfo(
+            context.getPackageName(), PackageInfoFlags.of(PackageManager.GET_PERMISSIONS));
+    String[] permissions = packageInfo.requestedPermissions;
+    assertThat(permissions).isNotNull();
+    assertThat(permissions).hasLength(4);
+  }
+
+  @Test
+  @Config(minSdk = TIRAMISU)
+  public void getPackageInfoAfterT_uninstalledPackage_includeUninstalled() throws Exception {
+    String packageName = context.getPackageName();
+    shadowOf(packageManager).deletePackage(packageName);
+
+    PackageInfo info =
+        packageManager.getPackageInfo(packageName, PackageInfoFlags.of(MATCH_UNINSTALLED_PACKAGES));
+    assertThat(info).isNotNull();
+    assertThat(info.packageName).isEqualTo(packageName);
+  }
+
+  @Test
+  @Config(minSdk = TIRAMISU)
+  public void getPackageInfoAfterT_uninstalledPackage_dontIncludeUninstalled() {
+    String packageName = context.getPackageName();
+    shadowOf(packageManager).deletePackage(packageName);
+
+    try {
+      PackageInfo info = packageManager.getPackageInfo(packageName, PackageInfoFlags.of(0));
+      fail("should have thrown NameNotFoundException:" + info.applicationInfo.flags);
+    } catch (NameNotFoundException e) {
+      // expected
+    }
+  }
+
+  @Test
+  @Config(minSdk = TIRAMISU)
+  public void getPackageInfoAfterT_disabledPackage_includeDisabled() throws Exception {
+    packageManager.setApplicationEnabledSetting(
+        context.getPackageName(), COMPONENT_ENABLED_STATE_DISABLED, 0);
+    PackageInfo info =
+        packageManager.getPackageInfo(
+            context.getPackageName(), PackageInfoFlags.of(MATCH_DISABLED_COMPONENTS));
+    assertThat(info).isNotNull();
+    assertThat(info.packageName).isEqualTo(context.getPackageName());
+  }
+
+  @Test
   public void getInstalledPackages_uninstalledPackage_includeUninstalled() {
     shadowOf(packageManager).deletePackage(context.getPackageName());
 
@@ -2064,6 +2115,45 @@
   }
 
   @Test
+  @Config(minSdk = TIRAMISU)
+  public void getInstalledPackagesAfterT_uninstalledPackage_includeUninstalled() {
+    shadowOf(packageManager).deletePackage(context.getPackageName());
+
+    assertThat(packageManager.getInstalledPackages(PackageInfoFlags.of(MATCH_UNINSTALLED_PACKAGES)))
+        .isNotEmpty();
+    assertThat(
+            packageManager
+                .getInstalledPackages(PackageInfoFlags.of(MATCH_UNINSTALLED_PACKAGES))
+                .get(0)
+                .packageName)
+        .isEqualTo(context.getPackageName());
+  }
+
+  @Test
+  @Config(minSdk = TIRAMISU)
+  public void getInstalledPackagesAfterT_uninstalledPackage_dontIncludeUninstalled() {
+    shadowOf(packageManager).deletePackage(context.getPackageName());
+
+    assertThat(packageManager.getInstalledPackages(PackageInfoFlags.of(0))).isEmpty();
+  }
+
+  @Test
+  @Config(minSdk = TIRAMISU)
+  public void getInstalledPackagesAfterT_disabledPackage_includeDisabled() {
+    packageManager.setApplicationEnabledSetting(
+        context.getPackageName(), COMPONENT_ENABLED_STATE_DISABLED, 0);
+
+    assertThat(packageManager.getInstalledPackages(PackageInfoFlags.of(MATCH_DISABLED_COMPONENTS)))
+        .isNotEmpty();
+    assertThat(
+            packageManager
+                .getInstalledPackages(PackageInfoFlags.of(MATCH_DISABLED_COMPONENTS))
+                .get(0)
+                .packageName)
+        .isEqualTo(context.getPackageName());
+  }
+
+  @Test
   public void testGetPreferredActivities() {
     final String packageName = "com.example.dummy";
     ComponentName name = new ComponentName(packageName, "LauncherActivity");
@@ -2390,6 +2480,24 @@
   }
 
   @Test
+  @Config(minSdk = TIRAMISU)
+  public void getPackageUid_sdkT() throws NameNotFoundException {
+    shadowOf(packageManager).setPackagesForUid(10, new String[] {"a_name"});
+    assertThat(packageManager.getPackageUid("a_name", PackageInfoFlags.of(0))).isEqualTo(10);
+  }
+
+  @Test
+  @Config(minSdk = TIRAMISU)
+  public void getPackageUid_sdkT_shouldThrowNameNotFoundExceptionIfNotExist() {
+    try {
+      packageManager.getPackageUid("a_name", PackageInfoFlags.of(0));
+      fail("should have thrown NameNotFoundException");
+    } catch (PackageManager.NameNotFoundException e) {
+      assertThat(e).hasMessageThat().contains("a_name");
+    }
+  }
+
+  @Test
   public void getPackagesForUid_shouldReturnSetPackageName() {
     shadowOf(packageManager).setPackagesForUid(10, new String[] {"a_name"});
     assertThat(packageManager.getPackagesForUid(10)).asList().containsExactly("a_name");
@@ -2637,7 +2745,7 @@
   }
 
   @Test
-  public void getInstalledApplications() {
+  public void getInstalledApplications_noFlags_oldSdk() {
     List<ApplicationInfo> installedApplications = packageManager.getInstalledApplications(0);
 
     // Default should include the application under test
@@ -2656,6 +2764,33 @@
   }
 
   @Test
+  @Config(minSdk = TIRAMISU)
+  public void getInstalledApplications_null_throwsException() {
+    assertThrows(Exception.class, () -> packageManager.getInstalledApplications(null));
+  }
+
+  @Test
+  @Config(minSdk = TIRAMISU)
+  public void getInstalledApplications_noFlags_returnsAllInstalledApplications() {
+    List<ApplicationInfo> installedApplications =
+        packageManager.getInstalledApplications(ApplicationInfoFlags.of(0));
+
+    // Default should include the application under test
+    assertThat(installedApplications).hasSize(1);
+    assertThat(installedApplications.get(0).packageName).isEqualTo("org.robolectric");
+
+    PackageInfo packageInfo = new PackageInfo();
+    packageInfo.packageName = "org.other";
+    packageInfo.applicationInfo = new ApplicationInfo();
+    packageInfo.applicationInfo.packageName = "org.other";
+    shadowOf(packageManager).installPackage(packageInfo);
+
+    installedApplications = packageManager.getInstalledApplications(0);
+    assertThat(installedApplications).hasSize(2);
+    assertThat(installedApplications.get(1).packageName).isEqualTo("org.other");
+  }
+
+  @Test
   public void getPermissionInfo() throws Exception {
     PermissionInfo permission =
         context.getPackageManager().getPermissionInfo("org.robolectric.some_permission", 0);
diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowPhoneWindowTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowPhoneWindowTest.java
index 019cc75..52721f0 100644
--- a/robolectric/src/test/java/org/robolectric/shadows/ShadowPhoneWindowTest.java
+++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowPhoneWindowTest.java
@@ -5,12 +5,14 @@
 
 import android.app.Activity;
 import android.graphics.drawable.Drawable;
+import android.os.Build.VERSION_CODES;
 import android.view.Window;
 import androidx.test.ext.junit.runners.AndroidJUnit4;
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.robolectric.Robolectric;
+import org.robolectric.annotation.Config;
 
 @RunWith(AndroidJUnit4.class)
 public class ShadowPhoneWindowTest {
@@ -36,4 +38,26 @@
     window.setBackgroundDrawable(drawable);
     assertThat(shadowOf(window).getBackgroundDrawable()).isSameInstanceAs(drawable);
   }
+
+  @Test
+  @Config(minSdk = VERSION_CODES.R)
+  public void getDecorFitsSystemWindows_noCall_returnsDefault() {
+    ShadowWindow candidate = shadowOf(window);
+    assertThat(candidate).isInstanceOf(ShadowPhoneWindow.class);
+
+    assertThat(((ShadowPhoneWindow) candidate).getDecorFitsSystemWindows()).isTrue();
+  }
+
+  @Test
+  @Config(minSdk = VERSION_CODES.R)
+  public void getDecorFitsSystemWindows_recordsLastValue() {
+    ShadowWindow candidate = shadowOf(window);
+    assertThat(candidate).isInstanceOf(ShadowPhoneWindow.class);
+
+    window.setDecorFitsSystemWindows(true);
+    assertThat(((ShadowPhoneWindow) candidate).getDecorFitsSystemWindows()).isTrue();
+
+    window.setDecorFitsSystemWindows(false);
+    assertThat(((ShadowPhoneWindow) candidate).getDecorFitsSystemWindows()).isFalse();
+  }
 }
\ No newline at end of file
diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowSensorManagerTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowSensorManagerTest.java
index ee02ce2..3f24171 100644
--- a/robolectric/src/test/java/org/robolectric/shadows/ShadowSensorManagerTest.java
+++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowSensorManagerTest.java
@@ -11,12 +11,16 @@
 import android.hardware.SensorDirectChannel;
 import android.hardware.SensorEvent;
 import android.hardware.SensorEventListener;
+import android.hardware.SensorEventListener2;
 import android.hardware.SensorManager;
 import android.os.Build;
+import android.os.Looper;
 import android.os.MemoryFile;
 import androidx.test.core.app.ApplicationProvider;
 import androidx.test.ext.junit.runners.AndroidJUnit4;
 import com.google.common.base.Optional;
+import java.util.ArrayList;
+import java.util.List;
 import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
@@ -226,8 +230,56 @@
     assertThat(sensorManager.getSensorList(0)).isNotNull();
   }
 
-  private static class TestSensorEventListener implements SensorEventListener {
+  @Test
+  @Config(minSdk = Build.VERSION_CODES.KITKAT)
+  public void flush_shouldCallOnFlushCompleted() {
+    Sensor accelSensor = ShadowSensor.newInstance(TYPE_ACCELEROMETER);
+    Sensor gyroSensor = ShadowSensor.newInstance(TYPE_GYROSCOPE);
+
+    TestSensorEventListener listener1 = new TestSensorEventListener();
+    TestSensorEventListener listener2 = new TestSensorEventListener();
+    TestSensorEventListener listener3 = new TestSensorEventListener();
+
+    sensorManager.registerListener(listener1, accelSensor, SensorManager.SENSOR_DELAY_NORMAL);
+    sensorManager.registerListener(listener2, accelSensor, SensorManager.SENSOR_DELAY_NORMAL);
+    sensorManager.registerListener(listener2, gyroSensor, SensorManager.SENSOR_DELAY_NORMAL);
+
+    // Call flush with the first listener. It should return true (as the flush
+    // succeeded), and should call onFlushCompleted for all listeners registered for accelSensor.
+    assertThat(sensorManager.flush(listener1)).isTrue();
+    shadowOf(Looper.getMainLooper()).idle();
+
+    assertThat(listener1.getOnFlushCompletedCalls()).containsExactly(accelSensor);
+    assertThat(listener2.getOnFlushCompletedCalls()).containsExactly(accelSensor);
+    assertThat(listener3.getOnFlushCompletedCalls()).isEmpty();
+
+    // Call flush with the second listener. It should again return true, and should call
+    // onFlushCompleted for all listeners registered for accelSensor and gyroSensor.
+    assertThat(sensorManager.flush(listener2)).isTrue();
+    shadowOf(Looper.getMainLooper()).idle();
+
+    // From the two calls to flush, onFlushCompleted should have been called twice for accelSensor
+    // and once for gyroSensor.
+    assertThat(listener1.getOnFlushCompletedCalls()).containsExactly(accelSensor, accelSensor);
+    assertThat(listener2.getOnFlushCompletedCalls())
+        .containsExactly(accelSensor, accelSensor, gyroSensor);
+    assertThat(listener3.getOnFlushCompletedCalls()).isEmpty();
+
+    // Call flush with the third listener. This listener is not registered for any sensors, so it
+    // should return false.
+    assertThat(sensorManager.flush(listener3)).isFalse();
+    shadowOf(Looper.getMainLooper()).idle();
+
+    // There should not have been any more onFlushCompleted calls.
+    assertThat(listener1.getOnFlushCompletedCalls()).containsExactly(accelSensor, accelSensor);
+    assertThat(listener2.getOnFlushCompletedCalls())
+        .containsExactly(accelSensor, accelSensor, gyroSensor);
+    assertThat(listener3.getOnFlushCompletedCalls()).isEmpty();
+  }
+
+  private static class TestSensorEventListener implements SensorEventListener2 {
     private Optional<SensorEvent> latestSensorEvent = Optional.absent();
+    private List<Sensor> onFlushCompletedCalls = new ArrayList<>();
 
     @Override
     public void onAccuracyChanged(Sensor sensor, int accuracy) {}
@@ -237,6 +289,15 @@
       latestSensorEvent = Optional.of(event);
     }
 
+    @Override
+    public void onFlushCompleted(Sensor sensor) {
+      onFlushCompletedCalls.add(sensor);
+    }
+
+    public List<Sensor> getOnFlushCompletedCalls() {
+      return onFlushCompletedCalls;
+    }
+
     public Optional<SensorEvent> getLatestSensorEvent() {
       return latestSensorEvent;
     }
diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowSubscriptionManagerTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowSubscriptionManagerTest.java
index 9ea0e9a..4e7fb16 100644
--- a/robolectric/src/test/java/org/robolectric/shadows/ShadowSubscriptionManagerTest.java
+++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowSubscriptionManagerTest.java
@@ -3,6 +3,8 @@
 import static android.content.Context.TELEPHONY_SUBSCRIPTION_SERVICE;
 import static android.os.Build.VERSION_CODES.N;
 import static android.os.Build.VERSION_CODES.P;
+import static android.os.Build.VERSION_CODES.R;
+import static android.os.Build.VERSION_CODES.TIRAMISU;
 import static androidx.test.core.app.ApplicationProvider.getApplicationContext;
 import static com.google.common.truth.Truth.assertThat;
 import static org.junit.Assert.assertThrows;
@@ -31,6 +33,14 @@
             getApplicationContext().getSystemService(TELEPHONY_SUBSCRIPTION_SERVICE);
   }
 
+  @Config(minSdk = R)
+  @Test
+  public void shouldGiveActiveDataSubscriptionId() {
+    int testId = 42;
+    ShadowSubscriptionManager.setActiveDataSubscriptionId(testId);
+    assertThat(SubscriptionManager.getActiveDataSubscriptionId()).isEqualTo(testId);
+  }
+
   @Test
   public void shouldGiveDefaultSubscriptionId() {
     int testId = 42;
@@ -161,24 +171,24 @@
 
   @Test
   public void isNetworkRoaming_shouldReturnTrueIfSet() {
-    shadowOf(subscriptionManager).setNetworkRoamingStatus(123, /*isNetworkRoaming=*/ true);
+    shadowOf(subscriptionManager).setNetworkRoamingStatus(123, /* isNetworkRoaming= */ true);
     assertThat(shadowOf(subscriptionManager).isNetworkRoaming(123)).isTrue();
   }
 
   /** Multi act-asserts are discouraged but here we are testing the set+unset. */
   @Test
   public void isNetworkRoaming_shouldReturnFalseIfUnset() {
-    shadowOf(subscriptionManager).setNetworkRoamingStatus(123, /*isNetworkRoaming=*/ true);
+    shadowOf(subscriptionManager).setNetworkRoamingStatus(123, /* isNetworkRoaming= */ true);
     assertThat(shadowOf(subscriptionManager).isNetworkRoaming(123)).isTrue();
 
-    shadowOf(subscriptionManager).setNetworkRoamingStatus(123, /*isNetworkRoaming=*/ false);
+    shadowOf(subscriptionManager).setNetworkRoamingStatus(123, /* isNetworkRoaming= */ false);
     assertThat(shadowOf(subscriptionManager).isNetworkRoaming(123)).isFalse();
   }
 
   /** Multi act-asserts are discouraged but here we are testing the set+clear. */
   @Test
   public void isNetworkRoaming_shouldReturnFalseOnClear() {
-    shadowOf(subscriptionManager).setNetworkRoamingStatus(123, /*isNetworkRoaming=*/ true);
+    shadowOf(subscriptionManager).setNetworkRoamingStatus(123, /* isNetworkRoaming= */ true);
     assertThat(shadowOf(subscriptionManager).isNetworkRoaming(123)).isTrue();
 
     shadowOf(subscriptionManager).clearNetworkRoamingStatus();
@@ -305,6 +315,22 @@
         .isEqualTo(123);
   }
 
+  @Test
+  @Config(minSdk = TIRAMISU)
+  public void getPhoneNumber_phoneNumberNotSet_returnsEmptyString() {
+    assertThat(subscriptionManager.getPhoneNumber(SubscriptionManager.DEFAULT_SUBSCRIPTION_ID))
+        .isEqualTo("");
+  }
+
+  @Test
+  @Config(minSdk = TIRAMISU)
+  public void getPhoneNumber_setPhoneNumber_returnsPhoneNumber() {
+    shadowOf(subscriptionManager)
+        .setPhoneNumber(SubscriptionManager.DEFAULT_SUBSCRIPTION_ID, "123");
+    assertThat(subscriptionManager.getPhoneNumber(SubscriptionManager.DEFAULT_SUBSCRIPTION_ID))
+        .isEqualTo("123");
+  }
+
   private static class DummySubscriptionsChangedListener
       extends SubscriptionManager.OnSubscriptionsChangedListener {
     private int subscriptionChangedCount;
diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowTelephonyManagerTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowTelephonyManagerTest.java
index 5f6d6b8..cb6359f 100644
--- a/robolectric/src/test/java/org/robolectric/shadows/ShadowTelephonyManagerTest.java
+++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowTelephonyManagerTest.java
@@ -33,6 +33,7 @@
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotEquals;
 import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertThrows;
 import static org.junit.Assert.assertTrue;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.reset;
@@ -45,6 +46,7 @@
 import static org.robolectric.shadows.ShadowTelephonyManager.createTelephonyDisplayInfo;
 
 import android.content.ComponentName;
+import android.content.Context;
 import android.content.Intent;
 import android.net.Uri;
 import android.os.Build.VERSION;
@@ -962,7 +964,7 @@
   @Test
   @Config(minSdk = S)
   public void setCallComposerStatus() {
-    ShadowTelephonyManager.setCallComposerStatus(CALL_COMPOSER_STATUS_ON);
+    telephonyManager.setCallComposerStatus(CALL_COMPOSER_STATUS_ON);
 
     assertThat(telephonyManager.getCallComposerStatus()).isEqualTo(CALL_COMPOSER_STATUS_ON);
   }
@@ -1030,4 +1032,12 @@
 
     assertThat(shadowOf(telephonyManager).getVisualVoicemailSmsFilterSettings()).isNull();
   }
+
+  @Test
+  @Config(minSdk = Q)
+  public void isEmergencyNumber_telephonyServiceUnavailable_throwsIllegalStateException() {
+    ShadowServiceManager.setServiceAvailability(Context.TELEPHONY_SERVICE, false);
+
+    assertThrows(IllegalStateException.class, () -> telephonyManager.isEmergencyNumber("911"));
+  }
 }
diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowTypefaceTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowTypefaceTest.java
index 47789de..c86c5e9 100644
--- a/robolectric/src/test/java/org/robolectric/shadows/ShadowTypefaceTest.java
+++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowTypefaceTest.java
@@ -162,7 +162,7 @@
     // This invokes the Typeface static initializer, which creates some default typefaces.
     Typeface.create("roboto", Typeface.BOLD);
     // Call the resetter to clear the FONTS map in Typeface
-    ShadowTypeface.reset();
+    ShadowLegacyTypeface.reset();
     Typeface typeface =
         new Typeface.CustomFallbackBuilder(family).setStyle(font.getStyle()).build();
     assertThat(typeface).isNotNull();
diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowUserManagerTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowUserManagerTest.java
index d165ec1..75df110 100644
--- a/robolectric/src/test/java/org/robolectric/shadows/ShadowUserManagerTest.java
+++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowUserManagerTest.java
@@ -7,7 +7,6 @@
 import static android.os.Build.VERSION_CODES.M;
 import static android.os.Build.VERSION_CODES.N;
 import static android.os.Build.VERSION_CODES.N_MR1;
-import static android.os.Build.VERSION_CODES.O;
 import static android.os.Build.VERSION_CODES.Q;
 import static android.os.Build.VERSION_CODES.R;
 import static com.google.common.truth.Truth.assertThat;
@@ -907,13 +906,13 @@
   }
 
   @Test
-  @Config(minSdk = O)
+  @Config(minSdk = N)
   public void isQuietModeEnabled_shouldReturnFalse() {
     assertThat(userManager.isQuietModeEnabled(Process.myUserHandle())).isFalse();
   }
 
   @Test
-  @Config(minSdk = Q)
+  @Config(minSdk = N)
   public void isQuietModeEnabled_withProfile_shouldReturnFalse() {
     shadowOf(userManager).addProfile(0, 10, "Work profile", UserInfo.FLAG_MANAGED_PROFILE);
 
@@ -921,6 +920,16 @@
   }
 
   @Test
+  @Config(minSdk = N)
+  public void isQuietModeEnabled_withProfileQuietMode_shouldReturnTrue() {
+    shadowOf(userManager)
+        .addProfile(
+            0, 10, "Work profile", UserInfo.FLAG_MANAGED_PROFILE | UserInfo.FLAG_QUIET_MODE);
+
+    assertThat(userManager.isQuietModeEnabled(new UserHandle(10))).isTrue();
+  }
+
+  @Test
   @Config(minSdk = Q)
   public void requestQuietModeEnabled_withoutPermission_shouldThrowException() {
     shadowOf(userManager).enforcePermissionChecks(true);
diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowViewRootImplTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowViewRootImplTest.java
new file mode 100644
index 0000000..e10cbb3
--- /dev/null
+++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowViewRootImplTest.java
@@ -0,0 +1,55 @@
+package org.robolectric.shadows;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.app.Activity;
+import android.os.Build;
+import android.view.View;
+import android.view.WindowInsets;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.Robolectric;
+import org.robolectric.android.controller.ActivityController;
+import org.robolectric.annotation.Config;
+
+@RunWith(AndroidJUnit4.class)
+public class ShadowViewRootImplTest {
+  private ActivityController<Activity> activityController;
+  private Activity activity;
+  private View rootView;
+
+  @Before
+  public void setUp() {
+    activityController = Robolectric.buildActivity(Activity.class);
+    activityController.setup();
+
+    activity = activityController.get();
+    rootView = activity.getWindow().getDecorView();
+  }
+
+  @Test
+  @Config(minSdk = Build.VERSION_CODES.R)
+  public void setIsStatusBarVisible_impactsGetWindowInsets() {
+    ShadowViewRootImpl.setIsStatusBarVisible(false);
+    WindowInsets windowInsets = rootView.getRootWindowInsets();
+    assertThat(windowInsets.isVisible(WindowInsets.Type.statusBars())).isFalse();
+
+    ShadowViewRootImpl.setIsStatusBarVisible(true);
+    windowInsets = rootView.getRootWindowInsets();
+    assertThat(windowInsets.isVisible(WindowInsets.Type.statusBars())).isTrue();
+  }
+
+  @Test
+  @Config(minSdk = Build.VERSION_CODES.R)
+  public void setIsNavigationBarVisible_impactsGetWindowInsets() {
+    ShadowViewRootImpl.setIsNavigationBarVisible(false);
+    WindowInsets windowInsets = rootView.getRootWindowInsets();
+    assertThat(windowInsets.isVisible(WindowInsets.Type.navigationBars())).isFalse();
+
+    ShadowViewRootImpl.setIsNavigationBarVisible(true);
+    windowInsets = rootView.getRootWindowInsets();
+    assertThat(windowInsets.isVisible(WindowInsets.Type.navigationBars())).isTrue();
+  }
+}
diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowWallpaperManagerTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowWallpaperManagerTest.java
index 89535f6..51e69ab 100644
--- a/robolectric/src/test/java/org/robolectric/shadows/ShadowWallpaperManagerTest.java
+++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowWallpaperManagerTest.java
@@ -3,7 +3,6 @@
 import static android.os.Build.VERSION_CODES.JELLY_BEAN_MR1;
 import static android.os.Build.VERSION_CODES.M;
 import static android.os.Build.VERSION_CODES.N;
-import static android.os.Build.VERSION_CODES.P;
 import static android.os.Build.VERSION_CODES.TIRAMISU;
 import static com.google.common.truth.Truth.assertThat;
 import static junit.framework.Assert.fail;
@@ -17,12 +16,11 @@
 import android.os.Bundle;
 import android.os.IBinder;
 import android.os.ParcelFileDescriptor;
-import androidx.annotation.Nullable;
 import androidx.test.core.app.ApplicationProvider;
 import androidx.test.ext.junit.runners.AndroidJUnit4;
+import com.google.common.io.ByteStreams;
 import java.io.ByteArrayInputStream;
 import java.io.ByteArrayOutputStream;
-import java.io.Closeable;
 import java.io.FileDescriptor;
 import java.io.FileInputStream;
 import java.io.IOException;
@@ -43,7 +41,7 @@
 
   private static final Bitmap TEST_IMAGE_3 = Bitmap.createBitmap(1, 5, Bitmap.Config.ARGB_8888);
 
-  private static final int UNSUPPORTED_FLAG = WallpaperManager.FLAG_LOCK + 123;
+  private static final int UNSUPPORTED_FLAG = 0x100; // neither FLAG_SYSTEM nor FLAG_LOCK
 
   private static final String SET_WALLPAPER_COMPONENT =
       "android.permission.SET_WALLPAPER_COMPONENT";
@@ -150,7 +148,7 @@
   }
 
   @Test
-  @Config(minSdk = P)
+  @Config(minSdk = N)
   public void setBitmap_flagSystem_shouldCacheInMemory() throws Exception {
     int returnCode =
         manager.setBitmap(
@@ -165,7 +163,7 @@
   }
 
   @Test
-  @Config(minSdk = P)
+  @Config(minSdk = N)
   public void setBitmap_liveWallpaperWasDefault_flagSystem_shouldRemoveLiveWallpaper()
       throws Exception {
     manager.setWallpaperComponent(TEST_WALLPAPER_SERVICE);
@@ -180,7 +178,7 @@
   }
 
   @Test
-  @Config(minSdk = P)
+  @Config(minSdk = N)
   public void setBitmap_multipleCallsWithFlagSystem_shouldCacheLastBitmapInMemory()
       throws Exception {
     manager.setBitmap(
@@ -204,7 +202,7 @@
   }
 
   @Test
-  @Config(minSdk = P)
+  @Config(minSdk = N)
   public void setBitmap_flagLock_shouldCacheInMemory() throws Exception {
     int returnCode =
         manager.setBitmap(
@@ -219,7 +217,7 @@
   }
 
   @Test
-  @Config(minSdk = P)
+  @Config(minSdk = N)
   public void setBitmap_liveWallpaperWasDefault_flagLock_shouldRemoveLiveWallpaper()
       throws Exception {
     manager.setWallpaperComponent(TEST_WALLPAPER_SERVICE);
@@ -234,7 +232,7 @@
   }
 
   @Test
-  @Config(minSdk = P)
+  @Config(minSdk = N)
   public void setBitmap_multipleCallsWithFlagLock_shouldCacheLastBitmapInMemory() throws Exception {
     manager.setBitmap(
         TEST_IMAGE_1,
@@ -257,7 +255,7 @@
   }
 
   @Test
-  @Config(minSdk = P)
+  @Config(minSdk = N)
   public void setBitmap_unsupportedFlag_shouldNotCacheInMemory() throws Exception {
     int code =
         manager.setBitmap(
@@ -268,7 +266,7 @@
   }
 
   @Test
-  @Config(minSdk = P)
+  @Config(minSdk = N)
   public void setBitmap_liveWallpaperWasDefault_unsupportedFlag_shouldNotRemoveLiveWallpaper()
       throws Exception {
     manager.setWallpaperComponent(TEST_WALLPAPER_SERVICE);
@@ -280,13 +278,13 @@
   }
 
   @Test
-  @Config(minSdk = P)
+  @Config(minSdk = N)
   public void getWallpaperFile_flagSystem_nothingCached_shouldReturnNull() throws Exception {
     assertThat(manager.getWallpaperFile(WallpaperManager.FLAG_SYSTEM)).isNull();
   }
 
   @Test
-  @Config(minSdk = P)
+  @Config(minSdk = N)
   public void getWallpaperFile_flagSystem_previouslyCached_shouldReturnParcelFileDescriptor()
       throws Exception {
     manager.setBitmap(
@@ -303,13 +301,13 @@
   }
 
   @Test
-  @Config(minSdk = P)
+  @Config(minSdk = N)
   public void getWallpaperFile_flagLock_nothingCached_shouldReturnNull() throws Exception {
     assertThat(manager.getWallpaperFile(WallpaperManager.FLAG_LOCK)).isNull();
   }
 
   @Test
-  @Config(minSdk = P)
+  @Config(minSdk = N)
   public void getWallpaperFile_flagLock_previouslyCached_shouldReturnParcelFileDescriptor()
       throws Exception {
     manager.setBitmap(
@@ -326,7 +324,7 @@
   }
 
   @Test
-  @Config(minSdk = P)
+  @Config(minSdk = N)
   public void getWallpaperFile_unsupportedFlag_shouldReturnNull() throws Exception {
     assertThat(manager.getWallpaperFile(UNSUPPORTED_FLAG)).isNull();
   }
@@ -366,61 +364,47 @@
   @Test
   @Config(minSdk = N)
   public void setStream_flagSystem_shouldCacheInMemory() throws Exception {
-    InputStream inputStream = null;
     byte[] testImageBytes = getBytesFromBitmap(TEST_IMAGE_1);
-    try {
-      inputStream = new ByteArrayInputStream(testImageBytes);
-      manager.setStream(
-          inputStream,
-          /* visibleCropHint= */ null,
-          /* allowBackup= */ true,
-          WallpaperManager.FLAG_SYSTEM);
+
+    manager.setStream(
+        new ByteArrayInputStream(testImageBytes),
+        /* visibleCropHint= */ null,
+        /* allowBackup= */ true,
+        WallpaperManager.FLAG_SYSTEM);
 
       assertThat(getBytesFromBitmap(shadowOf(manager).getBitmap(WallpaperManager.FLAG_SYSTEM)))
           .isEqualTo(testImageBytes);
       assertThat(shadowOf(manager).getBitmap(WallpaperManager.FLAG_LOCK)).isNull();
-    } finally {
-      close(inputStream);
-    }
   }
 
   @Test
   @Config(minSdk = N)
   public void setStream_flagLock_shouldCacheInMemory() throws Exception {
-    InputStream inputStream = null;
     byte[] testImageBytes = getBytesFromBitmap(TEST_IMAGE_2);
-    try {
-      inputStream = new ByteArrayInputStream(testImageBytes);
-      manager.setStream(
-          inputStream,
-          /* visibleCropHint= */ null,
-          /* allowBackup= */ true,
-          WallpaperManager.FLAG_LOCK);
+    manager.setStream(
+        new ByteArrayInputStream(testImageBytes),
+        /* visibleCropHint= */ null,
+        /* allowBackup= */ true,
+        WallpaperManager.FLAG_LOCK);
 
       assertThat(getBytesFromBitmap(shadowOf(manager).getBitmap(WallpaperManager.FLAG_LOCK)))
           .isEqualTo(testImageBytes);
       assertThat(shadowOf(manager).getBitmap(WallpaperManager.FLAG_SYSTEM)).isNull();
-    } finally {
-      close(inputStream);
-    }
   }
 
   @Test
   @Config(minSdk = N)
   public void setStream_unsupportedFlag_shouldNotCacheInMemory() throws Exception {
-    InputStream inputStream = null;
     byte[] testImageBytes = getBytesFromBitmap(TEST_IMAGE_2);
-    try {
-      inputStream = new ByteArrayInputStream(testImageBytes);
-      manager.setStream(
-          inputStream, /* visibleCropHint= */ null, /* allowBackup= */ true, UNSUPPORTED_FLAG);
+    manager.setStream(
+        new ByteArrayInputStream(testImageBytes),
+        /* visibleCropHint= */ null,
+        /* allowBackup= */ true,
+        UNSUPPORTED_FLAG);
 
       assertThat(shadowOf(manager).getBitmap(WallpaperManager.FLAG_LOCK)).isNull();
       assertThat(shadowOf(manager).getBitmap(WallpaperManager.FLAG_SYSTEM)).isNull();
       assertThat(shadowOf(manager).getBitmap(UNSUPPORTED_FLAG)).isNull();
-    } finally {
-      close(inputStream);
-    }
   }
 
   @Test
@@ -465,7 +449,7 @@
     assertThat(manager.getWallpaperInfo()).isNull();
   }
 
-  @Config(minSdk = P)
+  @Config(minSdk = N)
   public void
       getWallpaperInfo_staticWallpaperWasDefault_liveWallpaperSet_shouldRemoveCachedStaticWallpaper()
           throws Exception {
@@ -541,39 +525,48 @@
         .isEqualTo(1f);
   }
 
+  @Test
+  @Config(minSdk = N)
+  public void setBitmap_bothLockAndHome() throws Exception {
+    int returnCode =
+        manager.setBitmap(
+            TEST_IMAGE_1,
+            /* visibleCropHint= */ null,
+            /* allowBackup= */ false,
+            WallpaperManager.FLAG_SYSTEM | WallpaperManager.FLAG_LOCK);
+
+    assertThat(returnCode).isEqualTo(1);
+    assertThat(shadowOf(manager).getBitmap(WallpaperManager.FLAG_SYSTEM)).isEqualTo(TEST_IMAGE_1);
+    assertThat(shadowOf(manager).getBitmap(WallpaperManager.FLAG_LOCK)).isEqualTo(TEST_IMAGE_1);
+  }
+
+  @Test
+  @Config(minSdk = N)
+  public void setStream_bothLockAndHome() throws Exception {
+    byte[] testImageBytes = getBytesFromBitmap(TEST_IMAGE_1);
+    manager.setStream(
+        new ByteArrayInputStream(testImageBytes),
+        /* visibleCropHint= */ null,
+        /* allowBackup= */ true,
+        WallpaperManager.FLAG_SYSTEM | WallpaperManager.FLAG_LOCK);
+
+    assertThat(getBytesFromBitmap(shadowOf(manager).getBitmap(WallpaperManager.FLAG_SYSTEM)))
+        .isEqualTo(testImageBytes);
+    assertThat(getBytesFromBitmap(shadowOf(manager).getBitmap(WallpaperManager.FLAG_LOCK)))
+        .isEqualTo(testImageBytes);
+  }
+
   private static byte[] getBytesFromFileDescriptor(FileDescriptor fileDescriptor)
       throws IOException {
-    FileInputStream inputStream = null;
-    ByteArrayOutputStream outputStream = null;
-    try {
-      inputStream = new FileInputStream(fileDescriptor);
-      outputStream = new ByteArrayOutputStream();
-      byte[] buffer = new byte[1024];
-      int numOfBytes = 0;
-      while ((numOfBytes = inputStream.read(buffer, 0, buffer.length)) != -1) {
-        outputStream.write(buffer, 0, numOfBytes);
-      }
+    InputStream inputStream = new FileInputStream(fileDescriptor);
+    ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
+    ByteStreams.copy(inputStream, outputStream);
       return outputStream.toByteArray();
-    } finally {
-      close(inputStream);
-      close(outputStream);
-    }
   }
 
-  private static byte[] getBytesFromBitmap(Bitmap bitmap) throws IOException {
-    ByteArrayOutputStream stream = null;
-    try {
-      stream = new ByteArrayOutputStream();
-      bitmap.compress(Bitmap.CompressFormat.PNG, /* quality= */ 0, stream);
-      return stream.toByteArray();
-    } finally {
-      close(stream);
-    }
-  }
-
-  private static void close(@Nullable Closeable closeable) throws IOException {
-    if (closeable != null) {
-      closeable.close();
-    }
+  private static byte[] getBytesFromBitmap(Bitmap bitmap) {
+    ByteArrayOutputStream stream = new ByteArrayOutputStream();
+    bitmap.compress(Bitmap.CompressFormat.PNG, /* quality= */ 0, stream);
+    return stream.toByteArray();
   }
 }
diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowWifiManagerTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowWifiManagerTest.java
index 593327a..299a853 100644
--- a/robolectric/src/test/java/org/robolectric/shadows/ShadowWifiManagerTest.java
+++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowWifiManagerTest.java
@@ -675,6 +675,21 @@
   }
 
   @Test
+  @Config(minSdk = R)
+  public void testSetClearWifiConnectedNetworkScorer() {
+    // GIVEN
+    WifiManager.WifiConnectedNetworkScorer mockScorer =
+        mock(WifiManager.WifiConnectedNetworkScorer.class);
+    // WHEN
+    wifiManager.setWifiConnectedNetworkScorer(directExecutor(), mockScorer);
+    assertThat(shadowOf(wifiManager).isWifiConnectedNetworkScorerEnabled()).isTrue();
+    wifiManager.clearWifiConnectedNetworkScorer();
+
+    // THEN
+    assertThat(shadowOf(wifiManager).isWifiConnectedNetworkScorerEnabled()).isFalse();
+  }
+
+  @Test
   @Config(minSdk = Q)
   public void testGetUsabilityScores() {
     // GIVEN
diff --git a/sandbox/src/main/java/org/robolectric/config/AndroidConfigurer.java b/sandbox/src/main/java/org/robolectric/config/AndroidConfigurer.java
index 3a549a7..ce81f2a 100644
--- a/sandbox/src/main/java/org/robolectric/config/AndroidConfigurer.java
+++ b/sandbox/src/main/java/org/robolectric/config/AndroidConfigurer.java
@@ -60,6 +60,7 @@
         .doNotAcquirePackage("jdk.internal.")
         .doNotAcquirePackage("org.junit")
         .doNotAcquirePackage("org.hamcrest")
+        .doNotAcquirePackage("org.objectweb.asm")
         .doNotAcquirePackage("org.robolectric.annotation.")
         .doNotAcquirePackage("org.robolectric.internal.")
         .doNotAcquirePackage("org.robolectric.pluginapi.")
@@ -98,8 +99,7 @@
     }
 
     // Instrumenting these classes causes a weird failure.
-    builder.doNotInstrumentClass("android.R")
-        .doNotInstrumentClass("android.R$styleable");
+    builder.doNotInstrumentClass("android.R").doNotInstrumentClass("android.R$styleable");
 
     builder
         .addInstrumentedPackage("dalvik.")
diff --git a/sandbox/src/main/java/org/robolectric/internal/bytecode/ClassInstrumentor.java b/sandbox/src/main/java/org/robolectric/internal/bytecode/ClassInstrumentor.java
index 53872c1..00e2009 100644
--- a/sandbox/src/main/java/org/robolectric/internal/bytecode/ClassInstrumentor.java
+++ b/sandbox/src/main/java/org/robolectric/internal/bytecode/ClassInstrumentor.java
@@ -49,7 +49,7 @@
   private static final Handle BOOTSTRAP_STATIC;
   private static final Handle BOOTSTRAP_INTRINSIC;
   private static final String ROBO_INIT_METHOD_NAME = "$$robo$init";
-  static final Type OBJECT_TYPE = Type.getType(Object.class);
+  protected static final Type OBJECT_TYPE = Type.getType(Object.class);
   private static final ShadowImpl SHADOW_IMPL = new ShadowImpl();
   final Decorator decorator;
 
@@ -175,8 +175,6 @@
         // If there is no constructor, adds one
         addNoArgsConstructor(mutableClass);
 
-        addDirectCallConstructor(mutableClass);
-
         addRoboInitMethod(mutableClass);
 
         removeFinalFromFields(mutableClass);
@@ -236,20 +234,27 @@
    * Adds a call $$robo$init, which instantiates a shadow object if required. This is to support
    * custom shadows for Jacoco-instrumented classes (except cnstructor shadows).
    */
-  private void addCallToRoboInit(MutableClass mutableClass, MethodNode ctor) {
+  protected void addCallToRoboInit(MutableClass mutableClass, MethodNode ctor) {
     AbstractInsnNode returnNode =
         Iterables.find(
             ctor.instructions,
-            node -> node instanceof InsnNode && node.getOpcode() == Opcodes.RETURN,
+            node -> {
+              if (node.getOpcode() == Opcodes.INVOKESPECIAL) {
+                MethodInsnNode mNode = (MethodInsnNode) node;
+                return (mNode.owner.equals(mutableClass.internalClassName)
+                    || mNode.owner.equals(mutableClass.classNode.superName));
+              }
+              return false;
+            },
             null);
-    ctor.instructions.insertBefore(returnNode, new VarInsnNode(Opcodes.ALOAD, 0));
-    ctor.instructions.insertBefore(
+    ctor.instructions.insert(
         returnNode,
         new MethodInsnNode(
             Opcodes.INVOKEVIRTUAL,
             mutableClass.classType.getInternalName(),
             ROBO_INIT_METHOD_NAME,
             "()V"));
+    ctor.instructions.insert(returnNode, new VarInsnNode(Opcodes.ALOAD, 0));
   }
 
   private void instrumentMethods(MutableClass mutableClass) {
@@ -292,8 +297,6 @@
     }
   }
 
-  protected void addDirectCallConstructor(MutableClass mutableClass) {}
-
   /**
    * Generates code like this:
    *
@@ -351,12 +354,24 @@
   }
 
   /**
-   * Constructors are instrumented as follows: TODO(slliu): Fill in constructor instrumentation
-   * directions
+   * Constructors are instrumented as follows:
+   *
+   * <ul>
+   *   <li>The original constructor will be stripped of its instructions leading up to, and
+   *       including, the call to super() or this(). It is also renamed to $$robo$$__constructor__
+   *   <li>A method called __constructor__ is created and its job is to call
+   *       $$robo$$__constructor__. The __constructor__ method is what gets shadowed if a Shadow
+   *       wants to shadow a constructor.
+   *   <li>A new constructor is created and contains the stripped instructions of the original
+   *       constructor leading up to, and including, the call to super() or this(). Then, it has a
+   *       call to $$robo$init to initialize the Class' Shadow Object. Then, it uses invokedynamic
+   *       to call __constructor__. Finally, it contains any instructions that might occur after the
+   *       return statement in the original constructor.
+   * </ul>
    *
    * @param method the constructor to instrument
    */
-  private void instrumentConstructor(MutableClass mutableClass, MethodNode method) {
+  protected void instrumentConstructor(MutableClass mutableClass, MethodNode method) {
     makeMethodPrivate(method);
 
     InsnList callSuper = extractCallToSuperConstructor(mutableClass, method);
@@ -488,7 +503,8 @@
       instrumentNativeMethod(mutableClass, method);
     }
 
-    // todo figure out
+    // Create delegator method with same name as original method. The delegator method will use
+    // invokedynamic to decide at runtime whether to call original method or shadowed method
     String originalName = method.name;
     method.name = directMethodName(mutableClass, originalName);
 
@@ -505,7 +521,6 @@
     generator.endMethod();
     mutableClass.addMethod(delegatorMethodNode);
   }
-
   /**
    * Creates native stub which returns the default return value.
    *
@@ -715,6 +730,14 @@
     return Modifier.isStatic(m.access) ? Opcodes.H_INVOKESTATIC : Opcodes.H_INVOKESPECIAL;
   }
 
+  // implemented in DirectClassInstrumentor
+  public void setAndroidJarSDKVersion(int androidJarSDKVersion) {}
+
+  // implemented in DirectClassInstrumentor
+  protected int getAndroidJarSDKVersion() {
+    return -1;
+  }
+
   public interface Decorator {
     void decorate(MutableClass mutableClass);
   }
diff --git a/sandbox/src/main/java/org/robolectric/internal/bytecode/MutableClass.java b/sandbox/src/main/java/org/robolectric/internal/bytecode/MutableClass.java
index 305431f..63b4b20 100644
--- a/sandbox/src/main/java/org/robolectric/internal/bytecode/MutableClass.java
+++ b/sandbox/src/main/java/org/robolectric/internal/bytecode/MutableClass.java
@@ -54,6 +54,10 @@
     return new ArrayList<>(classNode.methods);
   }
 
+  public Type getClassType() {
+    return classType;
+  }
+
   public void addMethod(MethodNode methodNode) {
     classNode.methods.add(methodNode);
   }
diff --git a/sandbox/src/main/java/org/robolectric/internal/bytecode/ShadowMap.java b/sandbox/src/main/java/org/robolectric/internal/bytecode/ShadowMap.java
index 401cae1..cb77a1f 100644
--- a/sandbox/src/main/java/org/robolectric/internal/bytecode/ShadowMap.java
+++ b/sandbox/src/main/java/org/robolectric/internal/bytecode/ShadowMap.java
@@ -19,11 +19,11 @@
 /**
  * Maps from instrumented class to shadow class.
  *
- * We deal with class names rather than actual classes here, since a ShadowMap is built outside of
- * any sandboxes, but instrumented and shadowed classes must be loaded through a
- * {@link SandboxClassLoader}. We don't want to try to resolve those classes outside of a sandbox.
+ * <p>We deal with class names rather than actual classes here, since a ShadowMap is built outside
+ * of any sandboxes, but instrumented and shadowed classes must be loaded through a {@link
+ * SandboxClassLoader}. We don't want to try to resolve those classes outside of a sandbox.
  *
- * Once constructed, instances are immutable.
+ * <p>Once constructed, instances are immutable.
  */
 @SuppressWarnings("NewApi")
 public class ShadowMap {
@@ -69,6 +69,10 @@
     this.shadowPickers = ImmutableMap.copyOf(shadowPickers);
   }
 
+  public boolean hasShadowPicker(MutableClass mutableClass) {
+    return shadowPickers.containsKey(mutableClass.getName().replace('$', '.'));
+  }
+
   public ShadowInfo getShadowInfo(Class<?> clazz, ShadowMatcher shadowMatcher) {
     String instrumentedClassName = clazz.getName();
 
@@ -117,8 +121,8 @@
     return pickShadow(instrumentedClassName, clazz, shadowPickerClassName);
   }
 
-  private ShadowInfo pickShadow(String instrumentedClassName, Class<?> clazz,
-      String shadowPickerClassName) {
+  private ShadowInfo pickShadow(
+      String instrumentedClassName, Class<?> clazz, String shadowPickerClassName) {
     ClassLoader sandboxClassLoader = clazz.getClassLoader();
     try {
       Class<? extends ShadowPicker<?>> shadowPickerClass =
@@ -131,16 +135,22 @@
       ShadowInfo shadowInfo = obtainShadowInfo(selectedShadowClass);
 
       if (!shadowInfo.shadowedClassName.equals(instrumentedClassName)) {
-        throw new IllegalArgumentException("Implemented class for "
-            + selectedShadowClass.getName() + " (" + shadowInfo.shadowedClassName + ") != "
-            + instrumentedClassName);
+        throw new IllegalArgumentException(
+            "Implemented class for "
+                + selectedShadowClass.getName()
+                + " ("
+                + shadowInfo.shadowedClassName
+                + ") != "
+                + instrumentedClassName);
       }
 
       return shadowInfo;
-    } catch (ClassNotFoundException | NoSuchMethodException | InvocationTargetException
-        | IllegalAccessException | InstantiationException e) {
-      throw new RuntimeException("Failed to resolve shadow picker for " + instrumentedClassName,
-          e);
+    } catch (ClassNotFoundException
+        | NoSuchMethodException
+        | InvocationTargetException
+        | IllegalAccessException
+        | InstantiationException e) {
+      throw new RuntimeException("Failed to resolve shadow picker for " + instrumentedClassName, e);
     }
   }
 
@@ -227,7 +237,7 @@
     private final Map<String, ShadowInfo> overriddenShadows;
     private final Map<String, String> shadowPickers;
 
-    public Builder () {
+    public Builder() {
       defaultShadows = ImmutableListMultimap.of();
       overriddenShadows = new HashMap<>();
       shadowPickers = new HashMap<>();
@@ -265,8 +275,8 @@
     private void addShadowInfo(ShadowInfo shadowInfo) {
       overriddenShadows.put(shadowInfo.shadowedClassName, shadowInfo);
       if (shadowInfo.hasShadowPicker()) {
-        shadowPickers
-            .put(shadowInfo.shadowedClassName, shadowInfo.getShadowPickerClass().getName());
+        shadowPickers.put(
+            shadowInfo.shadowedClassName, shadowInfo.getShadowPickerClass().getName());
       }
     }
 
diff --git a/shadows/framework/build.gradle b/shadows/framework/build.gradle
index 21160b6..cd95bb1 100644
--- a/shadows/framework/build.gradle
+++ b/shadows/framework/build.gradle
@@ -55,7 +55,7 @@
     compileOnly(AndroidSdk.MAX_SDK.coordinates) { force = true }
     api "com.ibm.icu:icu4j:70.1"
     api "androidx.annotation:annotation:1.1.0"
-    api "com.google.auto.value:auto-value-annotations:1.9"
+    api "com.google.auto.value:auto-value-annotations:1.10"
     annotationProcessor "com.google.auto.value:auto-value:1.9"
 
     sqlite4java "com.almworks.sqlite4java:libsqlite4java-osx:$sqlite4javaVersion"
diff --git a/shadows/framework/src/main/java/android/media/Session2Token.java b/shadows/framework/src/main/java/android/media/Session2Token.java
deleted file mode 100644
index 4a321e7..0000000
--- a/shadows/framework/src/main/java/android/media/Session2Token.java
+++ /dev/null
@@ -1,10 +0,0 @@
-package android.media;
-
-/**
- * Temporary replacement for class missing in Android Q Preview 1.
- *
- * TODO: Remove for Q Preview 2.
- */
-public class Session2Token {
-
-}
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/GraphicsShadowPicker.java b/shadows/framework/src/main/java/org/robolectric/shadows/GraphicsShadowPicker.java
new file mode 100644
index 0000000..8916375
--- /dev/null
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/GraphicsShadowPicker.java
@@ -0,0 +1,32 @@
+package org.robolectric.shadows;
+
+import static android.os.Build.VERSION_CODES.O;
+
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.annotation.GraphicsMode;
+import org.robolectric.annotation.GraphicsMode.Mode;
+import org.robolectric.config.ConfigurationRegistry;
+import org.robolectric.shadow.api.ShadowPicker;
+
+/** A {@link ShadowPicker} that selects between shadows given the Graphics mode. */
+public class GraphicsShadowPicker<T> implements ShadowPicker<T> {
+
+  private final Class<? extends T> legacyShadowClass;
+  private final Class<? extends T> nativeShadowClass;
+
+  public GraphicsShadowPicker(
+      Class<? extends T> legacyShadowClass, Class<? extends T> nativeShadowClass) {
+    this.legacyShadowClass = legacyShadowClass;
+    this.nativeShadowClass = nativeShadowClass;
+  }
+
+  @Override
+  public Class<? extends T> pickShadowClass() {
+    if (RuntimeEnvironment.getApiLevel() >= O
+        && ConfigurationRegistry.get(GraphicsMode.Mode.class) == Mode.NATIVE) {
+      return nativeShadowClass;
+    } else {
+      return legacyShadowClass;
+    }
+  }
+}
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ImageUtil.java b/shadows/framework/src/main/java/org/robolectric/shadows/ImageUtil.java
index 75b4957..c9a723c 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ImageUtil.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ImageUtil.java
@@ -26,7 +26,6 @@
 import javax.imageio.ImageWriter;
 import javax.imageio.stream.ImageInputStream;
 import javax.imageio.stream.ImageOutputStream;
-import org.robolectric.Shadows;
 import org.robolectric.shadow.api.Shadow;
 
 public class ImageUtil {
@@ -117,7 +116,7 @@
     if (srcWidth <= 0 || srcHeight <= 0 || dstWidth <= 0 || dstHeight <= 0) {
       return false;
     }
-    BufferedImage before = ((ShadowBitmap) Shadow.extract(src)).getBufferedImage();
+    BufferedImage before = ((ShadowLegacyBitmap) Shadow.extract(src)).getBufferedImage();
     if (before == null || before.getColorModel() == null) {
       return false;
     }
@@ -129,7 +128,7 @@
         filter ? VALUE_INTERPOLATION_BILINEAR : VALUE_INTERPOLATION_NEAREST_NEIGHBOR);
     graphics2D.drawImage(before, 0, 0, dstWidth, dstHeight, 0, 0, srcWidth, srcHeight, null);
     graphics2D.dispose();
-    ((ShadowBitmap) Shadow.extract(dst)).setBufferedImage(after);
+    ((ShadowLegacyBitmap) Shadow.extract(dst)).setBufferedImage(after);
     return true;
   }
 
@@ -156,7 +155,8 @@
         int width = realBitmap.getWidth();
         int height = realBitmap.getHeight();
         boolean needAlphaChannel = needAlphaChannel(format);
-        BufferedImage bufferedImage = Shadows.shadowOf(realBitmap).getBufferedImage();
+        BufferedImage bufferedImage =
+            ((ShadowLegacyBitmap) Shadow.extract(realBitmap)).getBufferedImage();
         if (bufferedImage == null) {
           bufferedImage =
               new BufferedImage(
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ResponderLocationBuilder.java b/shadows/framework/src/main/java/org/robolectric/shadows/ResponderLocationBuilder.java
new file mode 100644
index 0000000..794e75d
--- /dev/null
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ResponderLocationBuilder.java
@@ -0,0 +1,211 @@
+package org.robolectric.shadows;
+
+import static org.robolectric.util.reflector.Reflector.reflector;
+
+import android.net.wifi.rtt.ResponderLocation;
+import org.robolectric.shadow.api.Shadow;
+import org.robolectric.util.reflector.Accessor;
+import org.robolectric.util.reflector.ForType;
+
+/** Builder for {@link ResponderLocation} */
+@SuppressWarnings("CanIgnoreReturnValueSuggester")
+public class ResponderLocationBuilder {
+  // LCI Subelement LCI state
+  private Double altitude;
+  private Double altitudeUncertainty;
+  private Integer altitudeType;
+  private Double latitudeDegrees;
+  private Double latitudeUncertainty;
+  private Double longitudeDegrees;
+  private Double longitudeUncertainty;
+  private Integer datum;
+  private Integer lciVersion;
+  private Boolean lciRegisteredLocationAgreement;
+
+  // LCI Subelement Z state
+  private Double heightAboveFloorMeters;
+  private Double heightAboveFloorUncertaintyMeters;
+  private Integer expectedToMove;
+  private Double floorNumber;
+
+  private ResponderLocationBuilder() {}
+
+  public static ResponderLocationBuilder newBuilder() {
+    return new ResponderLocationBuilder();
+  }
+
+  public ResponderLocationBuilder setAltitude(double altitude) {
+    this.altitude = altitude;
+    return this;
+  }
+
+  public ResponderLocationBuilder setAltitudeUncertainty(double altitudeUncertainty) {
+    this.altitudeUncertainty = altitudeUncertainty;
+    return this;
+  }
+
+  public ResponderLocationBuilder setAltitudeType(int altitudeType) {
+    this.altitudeType = altitudeType;
+    return this;
+  }
+
+  public ResponderLocationBuilder setLatitude(double latitudeDegrees) {
+    this.latitudeDegrees = latitudeDegrees;
+    return this;
+  }
+
+  public ResponderLocationBuilder setLatitudeUncertainty(double latitudeUncertainty) {
+    this.latitudeUncertainty = latitudeUncertainty;
+    return this;
+  }
+
+  public ResponderLocationBuilder setLongitude(double longitudeDegrees) {
+    this.longitudeDegrees = longitudeDegrees;
+    return this;
+  }
+
+  public ResponderLocationBuilder setLongitudeUncertainty(double longitudeUncertainty) {
+    this.longitudeUncertainty = longitudeUncertainty;
+    return this;
+  }
+
+  public ResponderLocationBuilder setDatum(int datum) {
+    this.datum = datum;
+    return this;
+  }
+
+  public ResponderLocationBuilder setLciVersion(int lciVersion) {
+    this.lciVersion = lciVersion;
+    return this;
+  }
+
+  public ResponderLocationBuilder setLciRegisteredLocationAgreement(
+      Boolean lciRegisteredLocationAgreement) {
+    this.lciRegisteredLocationAgreement = lciRegisteredLocationAgreement;
+    return this;
+  }
+
+  public ResponderLocationBuilder setHeightAboveFloorMeters(double heightAboveFloorMeters) {
+    this.heightAboveFloorMeters = heightAboveFloorMeters;
+    return this;
+  }
+
+  public ResponderLocationBuilder setHeightAboveFloorUncertaintyMeters(
+      double heightAboveFloorUncertaintyMeters) {
+    this.heightAboveFloorUncertaintyMeters = heightAboveFloorUncertaintyMeters;
+    return this;
+  }
+
+  public ResponderLocationBuilder setExpectedToMove(int expectedToMove) {
+    this.expectedToMove = expectedToMove;
+    return this;
+  }
+
+  public ResponderLocationBuilder setFloorNumber(double floorNumber) {
+    this.floorNumber = floorNumber;
+    return this;
+  }
+
+  public ResponderLocation build() {
+    ResponderLocation result = Shadow.newInstanceOf(ResponderLocation.class);
+
+    ResponderLocationReflector locationResponderReflector =
+        reflector(ResponderLocationReflector.class, result);
+
+    locationResponderReflector.setAltitude(this.altitude == null ? 0 : this.altitude);
+    locationResponderReflector.setAltitudeType(this.altitudeType == null ? 0 : this.altitudeType);
+    locationResponderReflector.setAltitudeUncertainty(
+        this.altitudeUncertainty == null ? 0 : this.altitudeUncertainty);
+    locationResponderReflector.setLatitude(this.latitudeDegrees == null ? 0 : this.latitudeDegrees);
+    locationResponderReflector.setLatitudeUncertainty(
+        this.latitudeUncertainty == null ? 0 : this.latitudeUncertainty);
+    locationResponderReflector.setLongitude(
+        this.longitudeDegrees == null ? 0 : this.longitudeDegrees);
+    locationResponderReflector.setLongitudeUncertainty(
+        this.longitudeUncertainty == null ? 0 : this.longitudeUncertainty);
+    locationResponderReflector.setDatum(this.datum == null ? 0 : this.datum);
+    locationResponderReflector.setLciVersion(this.lciVersion == null ? 0 : this.lciVersion);
+    locationResponderReflector.setLciRegisteredLocationAgreement(
+        this.lciRegisteredLocationAgreement != null && this.lciRegisteredLocationAgreement);
+    locationResponderReflector.setHeightAboveFloorMeters(
+        this.heightAboveFloorMeters == null ? 0 : this.heightAboveFloorMeters);
+    locationResponderReflector.setHeightAboveFloorUncertaintyMeters(
+        this.heightAboveFloorUncertaintyMeters == null
+            ? 0
+            : this.heightAboveFloorUncertaintyMeters);
+    locationResponderReflector.setExpectedToMove(
+        this.expectedToMove == null ? 0 : this.expectedToMove);
+    locationResponderReflector.setFloorNumber(this.floorNumber == null ? 0 : this.floorNumber);
+
+    locationResponderReflector.setIsLciValid(
+        this.altitude != null
+            && this.latitudeDegrees != null
+            && this.latitudeUncertainty != null
+            && this.longitudeDegrees != null
+            && this.longitudeUncertainty != null
+            && this.datum != null
+            && this.lciVersion != null
+            && this.lciRegisteredLocationAgreement != null
+            && this.altitudeType != null);
+
+    locationResponderReflector.setIsZValid(
+        this.heightAboveFloorMeters != null
+            && this.floorNumber != null
+            && this.expectedToMove != null
+            && this.heightAboveFloorUncertaintyMeters != null);
+
+    return result;
+  }
+
+  @ForType(ResponderLocation.class)
+  interface ResponderLocationReflector {
+
+    @Accessor("mAltitude")
+    void setAltitude(double altitude);
+
+    @Accessor("mAltitudeUncertainty")
+    void setAltitudeUncertainty(double altitudeUncertainty);
+
+    @Accessor("mAltitudeType")
+    void setAltitudeType(int altitudeType);
+
+    @Accessor("mLatitude")
+    void setLatitude(double latitudeDegrees);
+
+    @Accessor("mLatitudeUncertainty")
+    void setLatitudeUncertainty(double latitudeUncertainty);
+
+    @Accessor("mLongitude")
+    void setLongitude(double longitudeDegrees);
+
+    @Accessor("mLongitudeUncertainty")
+    void setLongitudeUncertainty(double longitudeUncertainty);
+
+    @Accessor("mDatum")
+    void setDatum(int datum);
+
+    @Accessor("mLciVersion")
+    void setLciVersion(int lciVersion);
+
+    @Accessor("mLciRegisteredLocationAgreement")
+    void setLciRegisteredLocationAgreement(boolean lciRegisteredLocationAgreement);
+
+    @Accessor("mHeightAboveFloorMeters")
+    void setHeightAboveFloorMeters(double heightAboveFloorMeters);
+
+    @Accessor("mHeightAboveFloorUncertaintyMeters")
+    void setHeightAboveFloorUncertaintyMeters(double heightAboveFloorUncertaintyMeters);
+
+    @Accessor("mExpectedToMove")
+    void setExpectedToMove(int expectedToMove);
+
+    @Accessor("mFloorNumber")
+    void setFloorNumber(double floorNumber);
+
+    @Accessor("mIsLciValid")
+    void setIsLciValid(boolean isLciValid);
+
+    @Accessor("mIsZValid")
+    void setIsZValid(boolean isZValid);
+  }
+}
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowAbstractCursor.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowAbstractCursor.java
deleted file mode 100644
index 24e3845..0000000
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowAbstractCursor.java
+++ /dev/null
@@ -1,23 +0,0 @@
-package org.robolectric.shadows;
-
-import android.database.AbstractCursor;
-import android.net.Uri;
-import org.robolectric.annotation.Implements;
-import org.robolectric.annotation.RealObject;
-import org.robolectric.util.ReflectionHelpers;
-
-@Implements(AbstractCursor.class)
-public class ShadowAbstractCursor {
-
-  @RealObject
-  private AbstractCursor realAbstractCursor;
-
-  /**
-   * Returns the Uri set by {@code setNotificationUri()}.
-   *
-   * @return Notification URI.
-   */
-  public Uri getNotificationUri_Compatibility() {
-    return ReflectionHelpers.getField(realAbstractCursor, "mNotifyUri");
-  }
-}
\ No newline at end of file
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowAccessibilityNodeInfo.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowAccessibilityNodeInfo.java
index bd9f509..a27d01c 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowAccessibilityNodeInfo.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowAccessibilityNodeInfo.java
@@ -416,7 +416,7 @@
     if (this.traversalBefore != null) {
       this.traversalBefore.recycle();
     }
-    
+
     this.traversalBefore = obtain(info);
   }
 
@@ -627,6 +627,7 @@
     }
     if (getApiLevel() >= P) {
       newInfo.setTooltipText(realAccessibilityNodeInfo.getTooltipText());
+      newInfo.setPaneTitle(realAccessibilityNodeInfo.getPaneTitle());
     }
 
     return newInfo;
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowAlarmManager.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowAlarmManager.java
index b43631c..ea551d8 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowAlarmManager.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowAlarmManager.java
@@ -1,40 +1,53 @@
 package org.robolectric.shadows;
 
 import static android.app.AlarmManager.RTC_WAKEUP;
-import static android.os.Build.VERSION_CODES.KITKAT;
-import static android.os.Build.VERSION_CODES.LOLLIPOP;
-import static android.os.Build.VERSION_CODES.M;
-import static android.os.Build.VERSION_CODES.N;
-import static android.os.Build.VERSION_CODES.S;
 import static org.robolectric.util.reflector.Reflector.reflector;
 
-import android.annotation.TargetApi;
 import android.app.AlarmManager;
 import android.app.AlarmManager.AlarmClockInfo;
 import android.app.AlarmManager.OnAlarmListener;
 import android.app.PendingIntent;
-import android.content.Intent;
+import android.app.PendingIntent.CanceledException;
+import android.os.Build.VERSION;
+import android.os.Build.VERSION_CODES;
 import android.os.Handler;
-import java.util.Collections;
+import android.os.Looper;
+import android.os.SystemClock;
+import android.os.WorkSource;
+import androidx.annotation.GuardedBy;
+import androidx.annotation.Nullable;
+import androidx.annotation.RequiresApi;
+import com.google.common.collect.Iterables;
+import java.util.ArrayList;
 import java.util.List;
+import java.util.Objects;
+import java.util.PriorityQueue;
 import java.util.TimeZone;
-import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.concurrent.Executor;
+import java.util.concurrent.RejectedExecutionException;
 import org.robolectric.annotation.Implementation;
 import org.robolectric.annotation.Implements;
 import org.robolectric.annotation.RealObject;
 import org.robolectric.annotation.Resetter;
-import org.robolectric.shadow.api.Shadow;
 import org.robolectric.util.reflector.Direct;
 import org.robolectric.util.reflector.ForType;
 
-@SuppressWarnings({"UnusedDeclaration"})
+/** Shadow for {@link android.app.AlarmManager}. */
 @Implements(AlarmManager.class)
 public class ShadowAlarmManager {
 
+  public static final long WINDOW_EXACT = 0;
+  public static final long WINDOW_HEURISTIC = -1;
+
   private static final TimeZone DEFAULT_TIMEZONE = TimeZone.getDefault();
 
   private static boolean canScheduleExactAlarms;
-  private final List<ScheduledAlarm> scheduledAlarms = new CopyOnWriteArrayList<>();
+  private static boolean autoSchedule;
+
+  private final Handler schedulingHandler = new Handler(Looper.getMainLooper());
+
+  @GuardedBy("scheduledAlarms")
+  private final PriorityQueue<InternalScheduledAlarm> scheduledAlarms = new PriorityQueue<>();
 
   @RealObject private AlarmManager realObject;
 
@@ -42,6 +55,291 @@
   public static void reset() {
     TimeZone.setDefault(DEFAULT_TIMEZONE);
     canScheduleExactAlarms = false;
+    autoSchedule = false;
+  }
+
+  /**
+   * When set to true, automatically schedules alarms to fire at the appropriate time (with respect
+   * to the main Looper time) when they are set. This means that a test as below could be expected
+   * to pass:
+   *
+   * <pre>{@code
+   * shadowOf(alarmManager).setAutoSchedule(true);
+   * AlarmManager.OnAlarmListener listener = mock(AlarmManager.OnAlarmListener.class);
+   * alarmManager.setExact(
+   *   ELAPSED_REALTIME_WAKEUP,
+   *   SystemClock.elapsedRealtime() + 10,
+   *   "tag",
+   *   listener,
+   *   new Handler(Looper.getMainLooper()));
+   * shadowOf(Looper.getMainLooper()).idleFor(Duration.ofMillis(10));
+   * verify(listener).onAlarm();
+   * }</pre>
+   *
+   * <p>Alarms are always scheduled with respect to the trigger/window start time - there is no
+   * emulation of alarms being reordered, rescheduled, or delayed, as might happen on a real device.
+   * If emulating this is necessary, see {@link #fireAlarm(ScheduledAlarm)}.
+   *
+   * <p>{@link AlarmManager.OnAlarmListener} alarms will be run on the correct Handler/Executor as
+   * specified when the alarm is set.
+   */
+  public static void setAutoSchedule(boolean autoSchedule) {
+    ShadowAlarmManager.autoSchedule = autoSchedule;
+  }
+
+  @Implementation
+  protected void set(int type, long triggerAtMs, PendingIntent operation) {
+    setImpl(type, triggerAtMs, WINDOW_HEURISTIC, 0L, operation, null, null, false);
+  }
+
+  @Implementation(minSdk = VERSION_CODES.N)
+  protected void set(
+      int type,
+      long triggerAtMs,
+      @Nullable String tag,
+      OnAlarmListener listener,
+      @Nullable Handler handler) {
+    setImpl(
+        type,
+        triggerAtMs,
+        WINDOW_HEURISTIC,
+        0L,
+        tag,
+        listener,
+        new HandlerExecutor(handler),
+        null,
+        false);
+  }
+
+  @Implementation
+  protected void setRepeating(
+      int type, long triggerAtMs, long intervalMs, PendingIntent operation) {
+    setImpl(type, triggerAtMs, WINDOW_HEURISTIC, intervalMs, operation, null, null, false);
+  }
+
+  @Implementation(minSdk = VERSION_CODES.KITKAT)
+  protected void setWindow(
+      int type, long windowStartMs, long windowLengthMs, PendingIntent operation) {
+    setImpl(type, windowStartMs, windowLengthMs, 0L, operation, null, null, false);
+  }
+
+  @Implementation(minSdk = VERSION_CODES.N)
+  protected void setWindow(
+      int type,
+      long windowStartMs,
+      long windowLengthMs,
+      @Nullable String tag,
+      OnAlarmListener listener,
+      @Nullable Handler handler) {
+    setImpl(
+        type,
+        windowStartMs,
+        windowLengthMs,
+        0L,
+        tag,
+        listener,
+        new HandlerExecutor(handler),
+        null,
+        false);
+  }
+
+  @Implementation(minSdk = 34)
+  protected void setWindow(
+      int type,
+      long windowStartMs,
+      long windowLengthMs,
+      @Nullable String tag,
+      Executor executor,
+      OnAlarmListener listener) {
+    setImpl(type, windowStartMs, windowLengthMs, 0L, tag, listener, executor, null, false);
+  }
+
+  @Implementation(minSdk = 34)
+  protected void setWindow(
+      int type,
+      long windowStartMs,
+      long windowLengthMs,
+      @Nullable String tag,
+      Executor executor,
+      WorkSource workSource,
+      OnAlarmListener listener) {
+    setImpl(type, windowStartMs, windowLengthMs, 0L, tag, listener, executor, workSource, false);
+  }
+
+  @Implementation(minSdk = VERSION_CODES.S)
+  protected void setPrioritized(
+      int type,
+      long windowStartMs,
+      long windowLengthMs,
+      @Nullable String tag,
+      Executor executor,
+      OnAlarmListener listener) {
+    Objects.requireNonNull(executor);
+    Objects.requireNonNull(listener);
+    setImpl(type, windowStartMs, windowLengthMs, 0L, tag, listener, executor, null, true);
+  }
+
+  @Implementation(minSdk = VERSION_CODES.KITKAT)
+  protected void setExact(int type, long triggerAtMs, PendingIntent operation) {
+    setImpl(type, triggerAtMs, WINDOW_EXACT, 0L, operation, null, null, false);
+  }
+
+  @Implementation(minSdk = VERSION_CODES.N)
+  protected void setExact(
+      int type,
+      long triggerAtTime,
+      @Nullable String tag,
+      OnAlarmListener listener,
+      @Nullable Handler targetHandler) {
+    setImpl(
+        type,
+        triggerAtTime,
+        WINDOW_EXACT,
+        0L,
+        tag,
+        listener,
+        new HandlerExecutor(targetHandler),
+        null,
+        false);
+  }
+
+  @RequiresApi(VERSION_CODES.LOLLIPOP)
+  @Implementation(minSdk = VERSION_CODES.LOLLIPOP)
+  protected void setAlarmClock(AlarmClockInfo info, PendingIntent operation) {
+    setImpl(RTC_WAKEUP, info.getTriggerTime(), WINDOW_EXACT, 0L, operation, null, info, true);
+  }
+
+  @Implementation(minSdk = VERSION_CODES.KITKAT)
+  protected void set(
+      int type,
+      long triggerAtMs,
+      long windowLengthMs,
+      long intervalMs,
+      PendingIntent operation,
+      @Nullable WorkSource workSource) {
+    setImpl(type, triggerAtMs, windowLengthMs, intervalMs, operation, workSource, null, false);
+  }
+
+  @Implementation(minSdk = VERSION_CODES.N)
+  protected void set(
+      int type,
+      long triggerAtMs,
+      long windowLengthMs,
+      long intervalMs,
+      @Nullable String tag,
+      OnAlarmListener listener,
+      @Nullable Handler targetHandler,
+      @Nullable WorkSource workSource) {
+    setImpl(
+        type,
+        triggerAtMs,
+        windowLengthMs,
+        intervalMs,
+        tag,
+        listener,
+        new HandlerExecutor(targetHandler),
+        workSource,
+        false);
+  }
+
+  @Implementation(minSdk = VERSION_CODES.N)
+  protected void set(
+      int type,
+      long triggerAtMs,
+      long windowLengthMs,
+      long intervalMs,
+      OnAlarmListener listener,
+      @Nullable Handler targetHandler,
+      @Nullable WorkSource workSource) {
+    setImpl(
+        type,
+        triggerAtMs,
+        windowLengthMs,
+        intervalMs,
+        null,
+        listener,
+        new HandlerExecutor(targetHandler),
+        workSource,
+        false);
+  }
+
+  @Implementation(minSdk = VERSION_CODES.S)
+  protected void setExact(
+      int type,
+      long triggerAtMs,
+      @Nullable String tag,
+      Executor executor,
+      WorkSource workSource,
+      OnAlarmListener listener) {
+    Objects.requireNonNull(workSource);
+    setImpl(type, triggerAtMs, WINDOW_EXACT, 0L, tag, listener, executor, workSource, false);
+  }
+
+  @Implementation
+  protected void setInexactRepeating(
+      int type, long triggerAtMs, long intervalMillis, PendingIntent operation) {
+    setImpl(type, triggerAtMs, WINDOW_HEURISTIC, intervalMillis, operation, null, null, false);
+  }
+
+  @Implementation(minSdk = VERSION_CODES.M)
+  protected void setAndAllowWhileIdle(int type, long triggerAtMs, PendingIntent operation) {
+    setImpl(type, triggerAtMs, WINDOW_HEURISTIC, 0L, operation, null, null, true);
+  }
+
+  @Implementation(minSdk = VERSION_CODES.M)
+  protected void setExactAndAllowWhileIdle(int type, long triggerAtMs, PendingIntent operation) {
+    setImpl(type, triggerAtMs, WINDOW_EXACT, 0L, operation, null, null, true);
+  }
+
+  @Implementation(minSdk = 34)
+  protected void setExactAndAllowWhileIdle(
+      int type,
+      long triggerAtMs,
+      @Nullable String tag,
+      Executor executor,
+      @Nullable WorkSource workSource,
+      OnAlarmListener listener) {
+    setImpl(type, triggerAtMs, WINDOW_EXACT, 0L, tag, listener, executor, workSource, true);
+  }
+
+  @Implementation
+  protected void cancel(PendingIntent operation) {
+    synchronized (scheduledAlarms) {
+      Iterables.removeIf(
+          scheduledAlarms,
+          alarm -> {
+            if (operation.equals(alarm.operation)) {
+              alarm.deschedule();
+              return true;
+            }
+            return false;
+          });
+    }
+  }
+
+  @Implementation(minSdk = VERSION_CODES.N)
+  protected void cancel(OnAlarmListener listener) {
+    synchronized (scheduledAlarms) {
+      Iterables.removeIf(
+          scheduledAlarms,
+          alarm -> {
+            if (listener.equals(alarm.onAlarmListener)) {
+              alarm.deschedule();
+              return true;
+            }
+            return false;
+          });
+    }
+  }
+
+  @Implementation(minSdk = 34)
+  protected void cancelAll() {
+    synchronized (scheduledAlarms) {
+      for (InternalScheduledAlarm alarm : scheduledAlarms) {
+        alarm.deschedule();
+      }
+      scheduledAlarms.clear();
+    }
   }
 
   @Implementation
@@ -52,257 +350,310 @@
     TimeZone.setDefault(TimeZone.getTimeZone(timeZone));
   }
 
-  @Implementation
-  protected void set(int type, long triggerAtTime, PendingIntent operation) {
-    internalSet(type, triggerAtTime, 0L, operation, null);
-  }
-
-  @Implementation(minSdk = N)
-  protected void set(
-      int type, long triggerAtTime, String tag, OnAlarmListener listener, Handler targetHandler) {
-    internalSet(type, triggerAtTime, listener, targetHandler);
-  }
-
-  @Implementation(minSdk = KITKAT)
-  protected void setExact(int type, long triggerAtTime, PendingIntent operation) {
-    internalSet(type, triggerAtTime, 0L, operation, null);
-  }
-
-  @Implementation(minSdk = N)
-  protected void setExact(
-      int type, long triggerAtTime, String tag, OnAlarmListener listener, Handler targetHandler) {
-    internalSet(type, triggerAtTime, listener, targetHandler);
-  }
-
-  @Implementation(minSdk = KITKAT)
-  protected void setWindow(
-      int type, long windowStartMillis, long windowLengthMillis, PendingIntent operation) {
-    internalSet(type, windowStartMillis, 0L, operation, null);
-  }
-
-  @Implementation(minSdk = N)
-  protected void setWindow(
-      int type,
-      long windowStartMillis,
-      long windowLengthMillis,
-      String tag,
-      OnAlarmListener listener,
-      Handler targetHandler) {
-    internalSet(type, windowStartMillis, listener, targetHandler);
-  }
-
-  @Implementation(minSdk = M)
-  protected void setAndAllowWhileIdle(int type, long triggerAtTime, PendingIntent operation) {
-    internalSet(type, triggerAtTime, 0L, operation, null, true);
-  }
-
-  @Implementation(minSdk = M)
-  protected void setExactAndAllowWhileIdle(int type, long triggerAtTime, PendingIntent operation) {
-    internalSet(type, triggerAtTime, 0L, operation, null, true);
-  }
-
-  @Implementation
-  protected void setRepeating(
-      int type, long triggerAtTime, long interval, PendingIntent operation) {
-    internalSet(type, triggerAtTime, interval, operation, null);
-  }
-
-  @Implementation
-  protected void setInexactRepeating(
-      int type, long triggerAtMillis, long intervalMillis, PendingIntent operation) {
-    internalSet(type, triggerAtMillis, intervalMillis, operation, null);
-  }
-
-  @Implementation(minSdk = LOLLIPOP)
-  protected void setAlarmClock(AlarmClockInfo info, PendingIntent operation) {
-    internalSet(RTC_WAKEUP, info.getTriggerTime(), 0L, operation, info.getShowIntent());
-  }
-
-  @Implementation(minSdk = LOLLIPOP)
-  protected AlarmClockInfo getNextAlarmClock() {
-    for (ScheduledAlarm scheduledAlarm : scheduledAlarms) {
-      AlarmClockInfo alarmClockInfo = scheduledAlarm.getAlarmClockInfo();
-      if (alarmClockInfo != null) {
-        return alarmClockInfo;
-      }
-    }
-    return null;
-  }
-
-  private void internalSet(
-      int type,
-      long triggerAtTime,
-      long interval,
-      PendingIntent operation,
-      PendingIntent showIntent) {
-    cancel(operation);
-    synchronized (scheduledAlarms) {
-      scheduledAlarms.add(new ScheduledAlarm(type, triggerAtTime, interval, operation, showIntent));
-      Collections.sort(scheduledAlarms);
-    }
-  }
-
-  private void internalSet(
-      int type,
-      long triggerAtTime,
-      long interval,
-      PendingIntent operation,
-      PendingIntent showIntent,
-      boolean allowWhileIdle) {
-    cancel(operation);
-    synchronized (scheduledAlarms) {
-      scheduledAlarms.add(
-          new ScheduledAlarm(type, triggerAtTime, interval, operation, showIntent, allowWhileIdle));
-      Collections.sort(scheduledAlarms);
-    }
-  }
-
-  private void internalSet(
-      int type, long triggerAtTime, OnAlarmListener listener, Handler handler) {
-    cancel(listener);
-    synchronized (scheduledAlarms) {
-      scheduledAlarms.add(new ScheduledAlarm(type, triggerAtTime, 0L, listener, handler));
-      Collections.sort(scheduledAlarms);
-    }
-  }
-
-  /** @return the next scheduled alarm after consuming it */
-  public ScheduledAlarm getNextScheduledAlarm() {
-    if (scheduledAlarms.isEmpty()) {
-      return null;
-    } else {
-      return scheduledAlarms.remove(0);
-    }
-  }
-
-  /** @return the most recently scheduled alarm without consuming it */
-  public ScheduledAlarm peekNextScheduledAlarm() {
-    if (scheduledAlarms.isEmpty()) {
-      return null;
-    } else {
-      return scheduledAlarms.get(0);
-    }
-  }
-
-  /** @return all scheduled alarms */
-  public List<ScheduledAlarm> getScheduledAlarms() {
-    return scheduledAlarms;
-  }
-
-  @Implementation
-  protected void cancel(PendingIntent operation) {
-    ShadowPendingIntent shadowPendingIntent = Shadow.extract(operation);
-    final Intent toRemove = shadowPendingIntent.getSavedIntent();
-    final int requestCode = shadowPendingIntent.getRequestCode();
-    for (ScheduledAlarm scheduledAlarm : scheduledAlarms) {
-      if (scheduledAlarm.operation != null) {
-        ShadowPendingIntent scheduledShadowPendingIntent = Shadow.extract(scheduledAlarm.operation);
-        final Intent scheduledIntent = scheduledShadowPendingIntent.getSavedIntent();
-        final int scheduledRequestCode = scheduledShadowPendingIntent.getRequestCode();
-        if (scheduledIntent.filterEquals(toRemove) && scheduledRequestCode == requestCode) {
-          scheduledAlarms.remove(scheduledAlarm);
-          break;
-        }
-      }
-    }
-  }
-
-  @Implementation(minSdk = N)
-  protected void cancel(OnAlarmListener listener) {
-    for (ScheduledAlarm scheduledAlarm : scheduledAlarms) {
-      if (scheduledAlarm.onAlarmListener != null) {
-        if (scheduledAlarm.onAlarmListener.equals(listener)) {
-          scheduledAlarms.remove(scheduledAlarm);
-          break;
-        }
-      }
-    }
-  }
-
-  /** Returns the schedule exact alarm state set by {@link #setCanScheduleExactAlarms}. */
-  @Implementation(minSdk = S)
+  @Implementation(minSdk = VERSION_CODES.S)
   protected boolean canScheduleExactAlarms() {
     return canScheduleExactAlarms;
   }
 
+  @RequiresApi(VERSION_CODES.LOLLIPOP)
+  @Implementation(minSdk = VERSION_CODES.LOLLIPOP)
+  @Nullable
+  protected AlarmClockInfo getNextAlarmClock() {
+    synchronized (scheduledAlarms) {
+      for (ScheduledAlarm scheduledAlarm : scheduledAlarms) {
+        AlarmClockInfo alarmClockInfo = scheduledAlarm.getAlarmClockInfo();
+        if (alarmClockInfo != null) {
+          return alarmClockInfo;
+        }
+      }
+      return null;
+    }
+  }
+
+  private void setImpl(
+      int type,
+      long triggerAtMs,
+      long windowLengthMs,
+      long intervalMs,
+      PendingIntent operation,
+      @Nullable WorkSource workSource,
+      @Nullable Object alarmClockInfo,
+      boolean allowWhileIdle) {
+    synchronized (scheduledAlarms) {
+      cancel(operation);
+      scheduledAlarms.add(
+          new InternalScheduledAlarm(
+                  type,
+                  triggerAtMs,
+                  windowLengthMs,
+                  intervalMs,
+                  operation,
+                  workSource,
+                  alarmClockInfo,
+                  allowWhileIdle)
+              .schedule());
+    }
+  }
+
+  private void setImpl(
+      int type,
+      long triggerAtMs,
+      long windowLengthMs,
+      long intervalMs,
+      @Nullable String tag,
+      OnAlarmListener listener,
+      Executor executor,
+      @Nullable WorkSource workSource,
+      boolean allowWhileIdle) {
+    synchronized (scheduledAlarms) {
+      cancel(listener);
+      scheduledAlarms.add(
+          new InternalScheduledAlarm(
+                  type,
+                  triggerAtMs,
+                  windowLengthMs,
+                  intervalMs,
+                  tag,
+                  listener,
+                  executor,
+                  workSource,
+                  null,
+                  allowWhileIdle)
+              .schedule());
+    }
+  }
+
   /**
-   * Sets the schedule exact alarm state reported by {@link AlarmManager#canScheduleExactAlarms},
+   * Returns the earliest scheduled alarm and removes it from the list of scheduled alarms.
+   *
+   * @deprecated Prefer to use {@link ShadowAlarmManager#setAutoSchedule(boolean)} in combination
+   *     with incrementing time to actually run alarms and test their side-effects.
+   */
+  @Deprecated
+  @Nullable
+  public ScheduledAlarm getNextScheduledAlarm() {
+    synchronized (scheduledAlarms) {
+      InternalScheduledAlarm alarm = scheduledAlarms.poll();
+      if (alarm != null) {
+        alarm.deschedule();
+      }
+      return alarm;
+    }
+  }
+
+  /** Returns the earliest scheduled alarm. */
+  @Nullable
+  public ScheduledAlarm peekNextScheduledAlarm() {
+    synchronized (scheduledAlarms) {
+      return scheduledAlarms.peek();
+    }
+  }
+
+  /** Returns a list of all scheduled alarms, ordered from earliest time to latest time. */
+  public List<ScheduledAlarm> getScheduledAlarms() {
+    synchronized (scheduledAlarms) {
+      return new ArrayList<>(scheduledAlarms);
+    }
+  }
+
+  /**
+   * Immediately removes the given alarm from the list of scheduled alarms (and then reschedules it
+   * in the case of a repeating alarm) and fires it. The given alarm must on the list of scheduled
+   * alarms prior to being fired.
+   *
+   * <p>Generally prefer to use {@link ShadowAlarmManager#setAutoSchedule(boolean)} in combination
+   * with advancing time on the main Looper in order to test alarms - however this method can be
+   * useful to emulate rescheduled, reordered, or delayed alarms, as may happen on a real device.
+   */
+  public void fireAlarm(ScheduledAlarm alarm) {
+    synchronized (scheduledAlarms) {
+      if (!scheduledAlarms.contains(alarm)) {
+        throw new IllegalArgumentException();
+      }
+
+      ((InternalScheduledAlarm) alarm).deschedule();
+      ((InternalScheduledAlarm) alarm).run();
+    }
+  }
+
+  /**
+   * Sets the schedule exact alarm state reported by {@link AlarmManager#canScheduleExactAlarms()},
    * but has no effect otherwise.
    */
   public static void setCanScheduleExactAlarms(boolean scheduleExactAlarms) {
     canScheduleExactAlarms = scheduleExactAlarms;
   }
 
-  /** Container object to hold a PendingIntent and parameters describing when to send it. */
+  /** Represents a set alarm. */
   public static class ScheduledAlarm implements Comparable<ScheduledAlarm> {
 
-    public final int type;
-    public final long triggerAtTime;
-    public final long interval;
-    public final PendingIntent operation;
-    public final boolean allowWhileIdle;
+    @Deprecated public final int type;
+    @Deprecated public final long triggerAtTime;
+    private final long windowLengthMs;
+    @Deprecated public final long interval;
+    @Nullable private final String tag;
+    @Deprecated @Nullable public final PendingIntent operation;
+    @Deprecated @Nullable public final OnAlarmListener onAlarmListener;
+    @Deprecated @Nullable public final Executor executor;
+    @Nullable private final WorkSource workSource;
+    @Nullable private final Object alarmClockInfo;
+    @Deprecated public final boolean allowWhileIdle;
 
-    // A non-null showIntent implies this alarm has a user interface. (i.e. in an alarm clock app)
-    public final PendingIntent showIntent;
+    @Deprecated @Nullable public final PendingIntent showIntent;
+    @Deprecated @Nullable public final Handler handler;
 
-    public final OnAlarmListener onAlarmListener;
-    public final Handler handler;
-
+    @Deprecated
     public ScheduledAlarm(
-        int type, long triggerAtTime, PendingIntent operation, PendingIntent showIntent) {
-      this(type, triggerAtTime, 0, operation, showIntent);
+        int type, long triggerAtMs, PendingIntent operation, PendingIntent showIntent) {
+      this(type, triggerAtMs, 0, operation, showIntent);
     }
 
+    @Deprecated
     public ScheduledAlarm(
         int type,
-        long triggerAtTime,
-        long interval,
+        long triggerAtMs,
+        long intervalMs,
         PendingIntent operation,
         PendingIntent showIntent) {
-      this(type, triggerAtTime, interval, operation, showIntent, null, null, false);
+      this(type, triggerAtMs, intervalMs, operation, showIntent, false);
     }
 
+    @Deprecated
     public ScheduledAlarm(
         int type,
-        long triggerAtTime,
-        long interval,
+        long triggerAtMs,
+        long intervalMs,
         PendingIntent operation,
         PendingIntent showIntent,
         boolean allowWhileIdle) {
-      this(type, triggerAtTime, interval, operation, showIntent, null, null, allowWhileIdle);
+      this(
+          type,
+          triggerAtMs,
+          intervalMs,
+          WINDOW_HEURISTIC,
+          operation,
+          null,
+          VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP && showIntent != null
+              ? new AlarmClockInfo(triggerAtMs, showIntent)
+              : null,
+          allowWhileIdle);
     }
 
-    private ScheduledAlarm(
+    protected ScheduledAlarm(
         int type,
-        long triggerAtTime,
-        long interval,
-        OnAlarmListener onAlarmListener,
-        Handler handler) {
-      this(type, triggerAtTime, interval, null, null, onAlarmListener, handler, false);
-    }
-
-    private ScheduledAlarm(
-        int type,
-        long triggerAtTime,
-        long interval,
+        long triggerAtMs,
+        long windowLengthMs,
+        long intervalMs,
         PendingIntent operation,
-        PendingIntent showIntent,
-        OnAlarmListener onAlarmListener,
-        Handler handler,
+        @Nullable WorkSource workSource,
+        @Nullable Object alarmClockInfo,
         boolean allowWhileIdle) {
       this.type = type;
-      this.triggerAtTime = triggerAtTime;
-      this.operation = operation;
-      this.interval = interval;
-      this.showIntent = showIntent;
-      this.onAlarmListener = onAlarmListener;
-      this.handler = handler;
+      this.triggerAtTime = triggerAtMs;
+      this.windowLengthMs = windowLengthMs;
+      this.interval = intervalMs;
+      this.tag = null;
+      this.operation = Objects.requireNonNull(operation);
+      this.onAlarmListener = null;
+      this.executor = null;
+      this.workSource = workSource;
+      this.alarmClockInfo = alarmClockInfo;
       this.allowWhileIdle = allowWhileIdle;
+
+      this.handler = null;
+      if (VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP && alarmClockInfo != null) {
+        this.showIntent = ((AlarmClockInfo) alarmClockInfo).getShowIntent();
+      } else {
+        this.showIntent = null;
+      }
     }
 
-    @TargetApi(LOLLIPOP)
+    protected ScheduledAlarm(
+        int type,
+        long triggerAtMs,
+        long windowLengthMs,
+        long intervalMs,
+        @Nullable String tag,
+        OnAlarmListener listener,
+        Executor executor,
+        @Nullable WorkSource workSource,
+        @Nullable Object alarmClockInfo,
+        boolean allowWhileIdle) {
+      this.type = type;
+      this.triggerAtTime = triggerAtMs;
+      this.windowLengthMs = windowLengthMs;
+      this.interval = intervalMs;
+      this.tag = tag;
+      this.operation = null;
+      this.onAlarmListener = Objects.requireNonNull(listener);
+      this.executor = Objects.requireNonNull(executor);
+      this.workSource = workSource;
+      this.alarmClockInfo = alarmClockInfo;
+      this.allowWhileIdle = allowWhileIdle;
+
+      if (executor instanceof HandlerExecutor) {
+        this.handler = ((HandlerExecutor) executor).handler;
+      } else {
+        this.handler = null;
+      }
+      if (VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP && alarmClockInfo != null) {
+        this.showIntent = ((AlarmClockInfo) alarmClockInfo).getShowIntent();
+      } else {
+        this.showIntent = null;
+      }
+    }
+
+    protected ScheduledAlarm(long triggerAtMs, ScheduledAlarm alarm) {
+      this.type = alarm.type;
+      this.triggerAtTime = triggerAtMs;
+      this.windowLengthMs = alarm.windowLengthMs;
+      this.interval = alarm.interval;
+      this.tag = alarm.tag;
+      this.operation = alarm.operation;
+      this.onAlarmListener = alarm.onAlarmListener;
+      this.executor = alarm.executor;
+      this.workSource = alarm.workSource;
+      this.alarmClockInfo = alarm.alarmClockInfo;
+      this.allowWhileIdle = alarm.allowWhileIdle;
+
+      this.handler = alarm.handler;
+      this.showIntent = alarm.showIntent;
+    }
+
+    public int getType() {
+      return type;
+    }
+
+    public long getTriggerAtMs() {
+      return triggerAtTime;
+    }
+
+    public long getWindowLengthMs() {
+      return windowLengthMs;
+    }
+
+    public long getIntervalMs() {
+      return interval;
+    }
+
+    @Nullable
+    public String getTag() {
+      return tag;
+    }
+
+    @Nullable
+    public WorkSource getWorkSource() {
+      return workSource;
+    }
+
+    @RequiresApi(VERSION_CODES.LOLLIPOP)
+    @Nullable
     public AlarmClockInfo getAlarmClockInfo() {
-      return showIntent == null ? null : new AlarmClockInfo(triggerAtTime, showIntent);
+      return (AlarmClockInfo) alarmClockInfo;
+    }
+
+    public boolean isAllowWhileIdle() {
+      return allowWhileIdle;
     }
 
     @Override
@@ -311,6 +662,119 @@
     }
   }
 
+  // wrapper class created because we can't modify ScheduledAlarm without breaking compatibility
+  private class InternalScheduledAlarm extends ScheduledAlarm implements Runnable {
+
+    InternalScheduledAlarm(
+        int type,
+        long triggerAtMs,
+        long windowLengthMs,
+        long intervalMs,
+        PendingIntent operation,
+        @Nullable WorkSource workSource,
+        @Nullable Object alarmClockInfo,
+        boolean allowWhileIdle) {
+      super(
+          type,
+          triggerAtMs,
+          windowLengthMs,
+          intervalMs,
+          operation,
+          workSource,
+          alarmClockInfo,
+          allowWhileIdle);
+    }
+
+    InternalScheduledAlarm(
+        int type,
+        long triggerAtMs,
+        long windowLengthMs,
+        long intervalMs,
+        @Nullable String tag,
+        OnAlarmListener listener,
+        Executor executor,
+        @Nullable WorkSource workSource,
+        @Nullable Object alarmClockInfo,
+        boolean allowWhileIdle) {
+      super(
+          type,
+          triggerAtMs,
+          windowLengthMs,
+          intervalMs,
+          tag,
+          listener,
+          executor,
+          workSource,
+          alarmClockInfo,
+          allowWhileIdle);
+    }
+
+    InternalScheduledAlarm(long triggerAtMs, InternalScheduledAlarm alarm) {
+      super(triggerAtMs, alarm);
+    }
+
+    InternalScheduledAlarm schedule() {
+      if (autoSchedule) {
+        schedulingHandler.postDelayed(this, triggerAtTime - SystemClock.elapsedRealtime());
+      }
+      return this;
+    }
+
+    void deschedule() {
+      schedulingHandler.removeCallbacks(this);
+    }
+
+    @Override
+    public void run() {
+      Executor executor;
+      if (operation != null) {
+        executor = Runnable::run;
+      } else {
+        executor = Objects.requireNonNull(this.executor);
+      }
+
+      executor.execute(
+          () -> {
+            synchronized (scheduledAlarms) {
+              if (!scheduledAlarms.remove(this)) {
+                return;
+              }
+              if (interval > 0) {
+                scheduledAlarms.add(
+                    new InternalScheduledAlarm(triggerAtTime + interval, this).schedule());
+              }
+            }
+            if (operation != null) {
+              try {
+                operation.send();
+              } catch (CanceledException e) {
+                // only necessary in case this is a repeated alarm and we've already rescheduled
+                cancel(operation);
+              }
+            } else if (VERSION.SDK_INT >= VERSION_CODES.N) {
+              Objects.requireNonNull(onAlarmListener).onAlarm();
+            } else {
+              throw new IllegalStateException();
+            }
+          });
+    }
+  }
+
+  private static final class HandlerExecutor implements Executor {
+    private final Handler handler;
+
+    HandlerExecutor(@Nullable Handler handler) {
+      this.handler = handler != null ? handler : new Handler(Looper.getMainLooper());
+    }
+
+    @Override
+    public void execute(Runnable command) {
+      if (!handler.post(command)) {
+        throw new RejectedExecutionException(handler + " is shutting down");
+      }
+    }
+  }
+
   @ForType(AlarmManager.class)
   interface AlarmManagerReflector {
 
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowAnimationUtils.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowAnimationUtils.java
index 8666c6b..10e2937 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowAnimationUtils.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowAnimationUtils.java
@@ -9,7 +9,6 @@
 import android.view.animation.TranslateAnimation;
 import org.robolectric.annotation.Implementation;
 import org.robolectric.annotation.Implements;
-import org.robolectric.shadow.api.Shadow;
 
 @SuppressWarnings({"UnusedDeclaration"})
 @Implements(AnimationUtils.class)
@@ -24,8 +23,6 @@
   protected static LayoutAnimationController loadLayoutAnimation(Context context, int id) {
     Animation anim = new TranslateAnimation(0, 0, 30, 0);
     LayoutAnimationController layoutAnim = new LayoutAnimationController(anim);
-    ShadowLayoutAnimationController shadowLayoutAnimationController = Shadow.extract(layoutAnim);
-    shadowLayoutAnimationController.setLoadedFromResourceId(id);
     return layoutAnim;
   }
 }
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowAppWidgetManager.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowAppWidgetManager.java
index cf93b9e..a8a0847 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowAppWidgetManager.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowAppWidgetManager.java
@@ -55,6 +55,10 @@
   private Multimap<UserHandle, AppWidgetProviderInfo> installedProvidersForProfile =
       HashMultimap.create();
 
+  // AppWidgetProvider is enabled if at least one widget is active. `isWidgetsEnabled` should be set
+  //  to false if the last widget is removed (when removing widgets is implemented).
+  private boolean isWidgetsEnabled = false;
+
   @Implementation(maxSdk = KITKAT)
   protected void __constructor__(Context context) {
     this.context = context;
@@ -307,6 +311,15 @@
     widgetInfo.view = widgetInfo.lastRemoteViews.apply(context, new AppWidgetHostView(context));
   }
 
+  private void enableWidgetsIfNecessary(Class<? extends AppWidgetProvider> appWidgetProviderClass) {
+    if (!isWidgetsEnabled) {
+      isWidgetsEnabled = true;
+      AppWidgetProvider appWidgetProvider =
+          ReflectionHelpers.callConstructor(appWidgetProviderClass);
+      appWidgetProvider.onReceive(context, new Intent(AppWidgetManager.ACTION_APPWIDGET_ENABLED));
+    }
+  }
+
   /**
    * Creates a widget by inflating its layout.
    *
@@ -345,7 +358,12 @@
       newWidgetIds[i] = myWidgetId;
     }
 
-    appWidgetProvider.onUpdate(context, realAppWidgetManager, newWidgetIds);
+    // Enable widgets if we are creating the first widget.
+    enableWidgetsIfNecessary(appWidgetProviderClass);
+
+    Intent intent = new Intent(AppWidgetManager.ACTION_APPWIDGET_UPDATE);
+    intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, newWidgetIds);
+    appWidgetProvider.onReceive(context, intent);
     return newWidgetIds;
   }
 
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowApplicationPackageManager.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowApplicationPackageManager.java
index 544d199..ae8dfb8 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowApplicationPackageManager.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowApplicationPackageManager.java
@@ -67,6 +67,7 @@
 import android.content.pm.PackageInfo;
 import android.content.pm.PackageItemInfo;
 import android.content.pm.PackageManager;
+import android.content.pm.PackageManager.ApplicationInfoFlags;
 import android.content.pm.PackageManager.NameNotFoundException;
 import android.content.pm.PackageManager.OnPermissionsChangedListener;
 import android.content.pm.PackageManager.PackageInfoFlags;
@@ -140,6 +141,15 @@
 
   @Implementation
   public List<PackageInfo> getInstalledPackages(int flags) {
+    return getInstalledPackages((long) flags);
+  }
+
+  @Implementation(minSdk = TIRAMISU)
+  protected List<PackageInfo> getInstalledPackages(Object flags) {
+    return getInstalledPackages(((PackageInfoFlags) flags).getValue());
+  }
+
+  private List<PackageInfo> getInstalledPackages(long flags) {
     List<PackageInfo> result = new ArrayList<>();
     synchronized (lock) {
       Set<String> packageNames = null;
@@ -429,6 +439,16 @@
 
   @Implementation
   protected PackageInfo getPackageInfo(String packageName, int flags) throws NameNotFoundException {
+    return getPackageInfo(packageName, (long) flags);
+  }
+
+  @Implementation(minSdk = TIRAMISU)
+  protected PackageInfo getPackageInfo(Object packageName, Object flags)
+      throws NameNotFoundException {
+    return getPackageInfo((String) packageName, ((PackageInfoFlags) flags).getValue());
+  }
+
+  private PackageInfo getPackageInfo(String packageName, long flags) throws NameNotFoundException {
     synchronized (lock) {
       PackageInfo info = packageInfos.get(packageName);
       if (info == null
@@ -494,7 +514,7 @@
   }
 
   private <T extends ComponentInfo> T[] applyFlagsToComponentInfoList(
-      T[] components, int flags, int activationFlag, Function<T, T> copyConstructor) {
+      T[] components, long flags, int activationFlag, Function<T, T> copyConstructor) {
     if (components == null || (flags & activationFlag) == 0) {
       return null;
     }
@@ -536,7 +556,7 @@
         || (VERSION.SDK_INT >= VERSION_CODES.KITKAT && resolveInfo.providerInfo != null);
   }
 
-  private static boolean isFlagSet(int flags, int matchFlag) {
+  private static boolean isFlagSet(long flags, long matchFlag) {
     return (flags & matchFlag) == matchFlag;
   }
 
@@ -859,6 +879,11 @@
         ActivityInfo::new);
   }
 
+  @Implementation(minSdk = TIRAMISU)
+  protected List<ResolveInfo> queryBroadcastReceivers(Object intent, @NonNull Object flags) {
+    return queryBroadcastReceivers((Intent) intent, (int) ((ResolveInfoFlags) flags).getValue());
+  }
+
   private static int matchIntentFilter(Intent intent, IntentFilter intentFilter) {
     return intentFilter.match(
         intent.getAction(),
@@ -891,7 +916,7 @@
    *
    * @throws NameNotFoundException when component is filtered out by a flag
    */
-  private void applyFlagsToComponentInfo(ComponentInfo componentInfo, int flags)
+  private void applyFlagsToComponentInfo(ComponentInfo componentInfo, long flags)
       throws NameNotFoundException {
     componentInfo.name = (componentInfo.name == null) ? "" : componentInfo.name;
     ApplicationInfo applicationInfo = componentInfo.applicationInfo;
@@ -975,6 +1000,15 @@
 
   @Implementation
   protected List<ApplicationInfo> getInstalledApplications(int flags) {
+    return getInstalledApplications((long) flags);
+  }
+
+  @Implementation(minSdk = TIRAMISU)
+  protected List<ApplicationInfo> getInstalledApplications(Object flags) {
+    return getInstalledApplications(((ApplicationInfoFlags) flags).getValue());
+  }
+
+  private List<ApplicationInfo> getInstalledApplications(long flags) {
     List<PackageInfo> packageInfos = getInstalledPackages(flags);
     List<ApplicationInfo> result = new ArrayList<>(packageInfos.size());
 
@@ -1305,6 +1339,11 @@
     return uid;
   }
 
+  @Implementation(minSdk = TIRAMISU)
+  protected Object getPackageUid(Object packageName, Object flags) throws NameNotFoundException {
+    return getPackageUid((String) packageName, (int) ((PackageInfoFlags) flags).getValue());
+  }
+
   @Implementation(minSdk = N)
   protected int getPackageUidAsUser(String packageName, int userId) throws NameNotFoundException {
     return 0;
@@ -1354,7 +1393,7 @@
     return packageInfo.applicationInfo;
   }
 
-  private void applyFlagsToApplicationInfo(@Nullable ApplicationInfo appInfo, int flags)
+  private void applyFlagsToApplicationInfo(@Nullable ApplicationInfo appInfo, long flags)
       throws NameNotFoundException {
     if (appInfo == null) {
       return;
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowAudioManager.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowAudioManager.java
index 183e9f3..2234351 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowAudioManager.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowAudioManager.java
@@ -14,6 +14,7 @@
 import android.annotation.RequiresPermission;
 import android.annotation.TargetApi;
 import android.media.AudioAttributes;
+import android.media.AudioDeviceCallback;
 import android.media.AudioDeviceInfo;
 import android.media.AudioFormat;
 import android.media.AudioManager;
@@ -74,6 +75,7 @@
       new HashSet<>();
   private final HashSet<AudioManager.AudioPlaybackCallback> audioPlaybackCallbacks =
       new HashSet<>();
+  private final HashSet<AudioDeviceCallback> audioDeviceCallbacks = new HashSet<>();
   private int ringerMode = AudioManager.RINGER_MODE_NORMAL;
   private int mode = AudioManager.MODE_NORMAL;
   private boolean bluetoothA2dpOn;
@@ -418,12 +420,123 @@
     defaultDevicesForAttributes = devices;
   }
 
+  /**
+   * Sets the list of connected input devices represented by {@link AudioDeviceInfo}.
+   *
+   * <p>The previous list of input devices is replaced and no notifications of the list of {@link
+   * AudioDeviceCallback} is done.
+   *
+   * <p>To add/remove devices one by one and trigger notifications for the list of {@link
+   * AudioDeviceCallback} please use one of the following methods {@link
+   * #addInputDevice(AudioDeviceInfo, boolean)}, {@link #removeInputDevice(AudioDeviceInfo,
+   * boolean)}.
+   */
   public void setInputDevices(List<AudioDeviceInfo> inputDevices) {
-    this.inputDevices = inputDevices;
+    this.inputDevices = new ArrayList<>(inputDevices);
   }
 
+  /**
+   * Sets the list of connected output devices represented by {@link AudioDeviceInfo}.
+   *
+   * <p>The previous list of output devices is replaced and no notifications of the list of {@link
+   * AudioDeviceCallback} is done.
+   *
+   * <p>To add/remove devices one by one and trigger notifications for the list of {@link
+   * AudioDeviceCallback} please use one of the following methods {@link
+   * #addOutputDevice(AudioDeviceInfo, boolean)}, {@link #removeOutputDevice(AudioDeviceInfo,
+   * boolean)}.
+   */
   public void setOutputDevices(List<AudioDeviceInfo> outputDevices) {
-    this.outputDevices = outputDevices;
+    this.outputDevices = new ArrayList<>(outputDevices);
+  }
+
+  /**
+   * Adds an input {@link AudioDeviceInfo} and notifies the list of {@link AudioDeviceCallback} if
+   * the device was not present before and indicated by {@code notifyAudioDeviceCallbacks}.
+   */
+  public void addInputDevice(AudioDeviceInfo inputDevice, boolean notifyAudioDeviceCallbacks) {
+    boolean changed =
+        !this.inputDevices.contains(inputDevice) && this.inputDevices.add(inputDevice);
+    if (changed && notifyAudioDeviceCallbacks) {
+      notifyAudioDeviceCallbacks(ImmutableList.of(inputDevice), /* added= */ true);
+    }
+  }
+
+  /**
+   * Removes an input {@link AudioDeviceInfo} and notifies the list of {@link AudioDeviceCallback}
+   * if the device was present before and indicated by {@code notifyAudioDeviceCallbacks}.
+   */
+  public void removeInputDevice(AudioDeviceInfo inputDevice, boolean notifyAudioDeviceCallbacks) {
+    boolean changed = this.inputDevices.remove(inputDevice);
+    if (changed && notifyAudioDeviceCallbacks) {
+      notifyAudioDeviceCallbacks(ImmutableList.of(inputDevice), /* added= */ false);
+    }
+  }
+
+  /**
+   * Adds an output {@link AudioDeviceInfo} and notifies the list of {@link AudioDeviceCallback} if
+   * the device was not present before and indicated by {@code notifyAudioDeviceCallbacks}.
+   */
+  public void addOutputDevice(AudioDeviceInfo outputDevice, boolean notifyAudioDeviceCallbacks) {
+    boolean changed =
+        !this.outputDevices.contains(outputDevice) && this.outputDevices.add(outputDevice);
+    if (changed && notifyAudioDeviceCallbacks) {
+      notifyAudioDeviceCallbacks(ImmutableList.of(outputDevice), /* added= */ true);
+    }
+  }
+
+  /**
+   * Removes an output {@link AudioDeviceInfo} and notifies the list of {@link AudioDeviceCallback}
+   * if the device was present before and indicated by {@code notifyAudioDeviceCallbacks}.
+   */
+  public void removeOutputDevice(AudioDeviceInfo outputDevice, boolean notifyAudioDeviceCallbacks) {
+    boolean changed = this.outputDevices.remove(outputDevice);
+    if (changed && notifyAudioDeviceCallbacks) {
+      notifyAudioDeviceCallbacks(ImmutableList.of(outputDevice), /* added= */ false);
+    }
+  }
+
+  /**
+   * Registers an {@link AudioDeviceCallback} object to receive notifications of changes to the set
+   * of connected audio devices.
+   *
+   * <p>The {@code handler} is ignored.
+   *
+   * @see #addInputDevice(AudioDeviceInfo, boolean)
+   * @see #addOutputDevice(AudioDeviceInfo, boolean)
+   * @see #removeInputDevice(AudioDeviceInfo, boolean)
+   * @see #removeOutputDevice(AudioDeviceInfo, boolean)
+   */
+  @Implementation(minSdk = M)
+  protected void registerAudioDeviceCallback(AudioDeviceCallback callback, Handler handler) {
+    audioDeviceCallbacks.add(callback);
+    // indicate currently available devices as added, similarly to MSG_DEVICES_CALLBACK_REGISTERED
+    callback.onAudioDevicesAdded(getDevices(AudioManager.GET_DEVICES_ALL));
+  }
+
+  /**
+   * Unregisters an {@link AudioDeviceCallback} object which has been previously registered to
+   * receive notifications of changes to the set of connected audio devices.
+   *
+   * @see #addInputDevice(AudioDeviceInfo, boolean)
+   * @see #addOutputDevice(AudioDeviceInfo, boolean)
+   * @see #removeInputDevice(AudioDeviceInfo, boolean)
+   * @see #removeOutputDevice(AudioDeviceInfo, boolean)
+   */
+  @Implementation(minSdk = M)
+  protected void unregisterAudioDeviceCallback(AudioDeviceCallback callback) {
+    audioDeviceCallbacks.remove(callback);
+  }
+
+  private void notifyAudioDeviceCallbacks(List<AudioDeviceInfo> devices, boolean added) {
+    AudioDeviceInfo[] devicesArray = devices.toArray(new AudioDeviceInfo[0]);
+    for (AudioDeviceCallback callback : audioDeviceCallbacks) {
+      if (added) {
+        callback.onAudioDevicesAdded(devicesArray);
+      } else {
+        callback.onAudioDevicesRemoved(devicesArray);
+      }
+    }
   }
 
   private List<AudioDeviceInfo> getInputDevices() {
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowBackdropFrameRenderer.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowBackdropFrameRenderer.java
index 2beaab0..38243e4 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowBackdropFrameRenderer.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowBackdropFrameRenderer.java
@@ -11,7 +11,6 @@
 import org.robolectric.annotation.Implements;
 import org.robolectric.annotation.RealObject;
 import org.robolectric.util.reflector.Accessor;
-import org.robolectric.util.reflector.Direct;
 import org.robolectric.util.reflector.ForType;
 
 /** Shadow for {@link BackdropFrameRenderer} */
@@ -49,7 +48,6 @@
 
   @ForType(BackdropFrameRenderer.class)
   interface BackdropFrameRendererReflector {
-    @Direct
     void releaseRenderer();
 
     @Accessor("mRenderer")
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowBitmap.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowBitmap.java
index 1b35839..7413308 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowBitmap.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowBitmap.java
@@ -1,80 +1,15 @@
 package org.robolectric.shadows;
 
-import static android.os.Build.VERSION_CODES.JELLY_BEAN_MR1;
-import static android.os.Build.VERSION_CODES.KITKAT;
-import static android.os.Build.VERSION_CODES.M;
-import static android.os.Build.VERSION_CODES.O;
-import static android.os.Build.VERSION_CODES.Q;
-import static android.os.Build.VERSION_CODES.S;
-import static com.google.common.base.Preconditions.checkArgument;
-import static com.google.common.base.Preconditions.checkNotNull;
-import static java.lang.Integer.max;
-import static java.lang.Integer.min;
-
 import android.graphics.Bitmap;
-import android.graphics.ColorSpace;
 import android.graphics.Matrix;
-import android.graphics.Paint;
-import android.graphics.Rect;
-import android.graphics.RectF;
-import android.os.Build;
-import android.os.Parcel;
-import android.util.DisplayMetrics;
-import java.awt.Color;
-import java.awt.Graphics2D;
-import java.awt.geom.Rectangle2D;
-import java.awt.image.BufferedImage;
-import java.awt.image.ColorModel;
-import java.awt.image.DataBufferInt;
-import java.awt.image.WritableRaster;
-import java.io.FileDescriptor;
 import java.io.InputStream;
-import java.io.OutputStream;
-import java.nio.Buffer;
-import java.nio.ByteBuffer;
-import java.nio.IntBuffer;
-import java.util.Arrays;
-import org.robolectric.RuntimeEnvironment;
-import org.robolectric.Shadows;
-import org.robolectric.annotation.Implementation;
 import org.robolectric.annotation.Implements;
-import org.robolectric.annotation.RealObject;
 import org.robolectric.shadow.api.Shadow;
-import org.robolectric.util.ReflectionHelpers;
+import org.robolectric.shadows.ShadowBitmap.Picker;
 
-@SuppressWarnings({"UnusedDeclaration"})
-@Implements(Bitmap.class)
-public class ShadowBitmap {
-  /** Number of bytes used internally to represent each pixel */
-  private static final int INTERNAL_BYTES_PER_PIXEL = 4;
-
-  int createdFromResId = -1;
-  String createdFromPath;
-  InputStream createdFromStream;
-  FileDescriptor createdFromFileDescriptor;
-  byte[] createdFromBytes;
-  @RealObject private Bitmap realBitmap;
-  private Bitmap createdFromBitmap;
-  private Bitmap scaledFromBitmap;
-  private int createdFromX = -1;
-  private int createdFromY = -1;
-  private int createdFromWidth = -1;
-  private int createdFromHeight = -1;
-  private int[] createdFromColors;
-  private Matrix createdFromMatrix;
-  private boolean createdFromFilter;
-
-  private int width;
-  private int height;
-  private BufferedImage bufferedImage;
-  private Bitmap.Config config;
-  private boolean mutable = true;
-  private String description = "";
-  private boolean recycled = false;
-  private boolean hasMipMap;
-  private boolean requestPremultiplied = true;
-  private boolean hasAlpha;
-  private ColorSpace colorSpace;
+/** Base class for {@link Bitmap} shadows. */
+@Implements(value = Bitmap.class, shadowPicker = Picker.class)
+public abstract class ShadowBitmap {
 
   /**
    * Returns a textual representation of the appearance of the object.
@@ -87,264 +22,13 @@
     return shadowBitmap.getDescription();
   }
 
-  @Implementation
-  protected static Bitmap createBitmap(int width, int height, Bitmap.Config config) {
-    return createBitmap((DisplayMetrics) null, width, height, config);
-  }
-
-  @Implementation(minSdk = JELLY_BEAN_MR1)
-  protected static Bitmap createBitmap(
-      DisplayMetrics displayMetrics, int width, int height, Bitmap.Config config) {
-    return createBitmap(displayMetrics, width, height, config, true);
-  }
-
-  @Implementation(minSdk = JELLY_BEAN_MR1)
-  protected static Bitmap createBitmap(
-      DisplayMetrics displayMetrics,
-      int width,
-      int height,
-      Bitmap.Config config,
-      boolean hasAlpha) {
-    if (width <= 0 || height <= 0) {
-      throw new IllegalArgumentException("width and height must be > 0");
-    }
-    checkNotNull(config);
-    Bitmap scaledBitmap = ReflectionHelpers.callConstructor(Bitmap.class);
-    ShadowBitmap shadowBitmap = Shadow.extract(scaledBitmap);
-    shadowBitmap.setDescription("Bitmap (" + width + " x " + height + ")");
-
-    shadowBitmap.width = width;
-    shadowBitmap.height = height;
-    shadowBitmap.config = config;
-    shadowBitmap.hasAlpha = hasAlpha;
-    shadowBitmap.setMutable(true);
-    if (displayMetrics != null) {
-      scaledBitmap.setDensity(displayMetrics.densityDpi);
-    }
-    shadowBitmap.bufferedImage = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
-    if (RuntimeEnvironment.getApiLevel() >= O) {
-      shadowBitmap.colorSpace = ColorSpace.get(ColorSpace.Named.SRGB);
-    }
-    return scaledBitmap;
-  }
-
-  @Implementation(minSdk = O)
-  protected static Bitmap createBitmap(
-      int width, int height, Bitmap.Config config, boolean hasAlpha, ColorSpace colorSpace) {
-    checkArgument(colorSpace != null || config == Bitmap.Config.ALPHA_8);
-    Bitmap bitmap = createBitmap(null, width, height, config, hasAlpha);
-    ShadowBitmap shadowBitmap = Shadows.shadowOf(bitmap);
-    shadowBitmap.colorSpace = colorSpace;
-    return bitmap;
-  }
-
-  @Implementation
-  protected static Bitmap createBitmap(
-      Bitmap src, int x, int y, int width, int height, Matrix matrix, boolean filter) {
-    if (x == 0
-        && y == 0
-        && width == src.getWidth()
-        && height == src.getHeight()
-        && (matrix == null || matrix.isIdentity())) {
-      return src; // Return the original.
-    }
-
-    if (x + width > src.getWidth()) {
-      throw new IllegalArgumentException("x + width must be <= bitmap.width()");
-    }
-    if (y + height > src.getHeight()) {
-      throw new IllegalArgumentException("y + height must be <= bitmap.height()");
-    }
-
-    Bitmap newBitmap = ReflectionHelpers.callConstructor(Bitmap.class);
-    ShadowBitmap shadowNewBitmap = Shadow.extract(newBitmap);
-
-    ShadowBitmap shadowSrcBitmap = Shadow.extract(src);
-    shadowNewBitmap.appendDescription(shadowSrcBitmap.getDescription());
-    shadowNewBitmap.appendDescription(" at (" + x + "," + y + ")");
-    shadowNewBitmap.appendDescription(" with width " + width + " and height " + height);
-
-    shadowNewBitmap.createdFromBitmap = src;
-    shadowNewBitmap.createdFromX = x;
-    shadowNewBitmap.createdFromY = y;
-    shadowNewBitmap.createdFromWidth = width;
-    shadowNewBitmap.createdFromHeight = height;
-    shadowNewBitmap.createdFromMatrix = matrix;
-    shadowNewBitmap.createdFromFilter = filter;
-    shadowNewBitmap.config = src.getConfig();
-    if (matrix != null) {
-      ShadowMatrix shadowMatrix = Shadow.extract(matrix);
-      shadowNewBitmap.appendDescription(" using matrix " + shadowMatrix.getDescription());
-
-      // Adjust width and height by using the matrix.
-      RectF mappedRect = new RectF();
-      matrix.mapRect(mappedRect, new RectF(0, 0, width, height));
-      width = Math.round(mappedRect.width());
-      height = Math.round(mappedRect.height());
-    }
-    if (filter) {
-      shadowNewBitmap.appendDescription(" with filter");
-    }
-
-    // updated if matrix is non-null
-    shadowNewBitmap.width = width;
-    shadowNewBitmap.height = height;
-    shadowNewBitmap.setMutable(true);
-    newBitmap.setDensity(src.getDensity());
-    if ((matrix == null || matrix.isIdentity()) && shadowSrcBitmap.bufferedImage != null) {
-      // Only simple cases are supported for setting image data to the new Bitmap.
-      shadowNewBitmap.bufferedImage =
-          shadowSrcBitmap.bufferedImage.getSubimage(x, y, width, height);
-    }
-    if (RuntimeEnvironment.getApiLevel() >= O) {
-      shadowNewBitmap.colorSpace = shadowSrcBitmap.colorSpace;
-    }
-    return newBitmap;
-  }
-
-  @Implementation
-  protected static Bitmap createBitmap(
-      int[] colors, int offset, int stride, int width, int height, Bitmap.Config config) {
-    return createBitmap(null, colors, offset, stride, width, height, config);
-  }
-
-  @Implementation(minSdk = JELLY_BEAN_MR1)
-  protected static Bitmap createBitmap(
-      DisplayMetrics displayMetrics,
-      int[] colors,
-      int offset,
-      int stride,
-      int width,
-      int height,
-      Bitmap.Config config) {
-    if (width <= 0) {
-      throw new IllegalArgumentException("width must be > 0");
-    }
-    if (height <= 0) {
-      throw new IllegalArgumentException("height must be > 0");
-    }
-    if (Math.abs(stride) < width) {
-      throw new IllegalArgumentException("abs(stride) must be >= width");
-    }
-    checkNotNull(config);
-    int lastScanline = offset + (height - 1) * stride;
-    int length = colors.length;
-    if (offset < 0
-        || (offset + width > length)
-        || lastScanline < 0
-        || (lastScanline + width > length)) {
-      throw new ArrayIndexOutOfBoundsException();
-    }
-
-    BufferedImage bufferedImage = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
-    bufferedImage.setRGB(0, 0, width, height, colors, offset, stride);
-    Bitmap bitmap = createBitmap(bufferedImage, width, height, config);
-    ShadowBitmap shadowBitmap = Shadow.extract(bitmap);
-    shadowBitmap.setMutable(false);
-    shadowBitmap.createdFromColors = colors;
-    if (displayMetrics != null) {
-      bitmap.setDensity(displayMetrics.densityDpi);
-    }
-    if (RuntimeEnvironment.getApiLevel() >= O) {
-      shadowBitmap.colorSpace = ColorSpace.get(ColorSpace.Named.SRGB);
-    }
-    return bitmap;
-  }
-
-  private static Bitmap createBitmap(
-      BufferedImage bufferedImage, int width, int height, Bitmap.Config config) {
-    Bitmap newBitmap = Bitmap.createBitmap(width, height, config);
-    ShadowBitmap shadowBitmap = Shadow.extract(newBitmap);
-    shadowBitmap.bufferedImage = bufferedImage;
-    return newBitmap;
-  }
-
-  @Implementation
-  protected static Bitmap createScaledBitmap(
-      Bitmap src, int dstWidth, int dstHeight, boolean filter) {
-    if (dstWidth == src.getWidth() && dstHeight == src.getHeight() && !filter) {
-      return src; // Return the original.
-    }
-    if (dstWidth <= 0 || dstHeight <= 0) {
-      throw new IllegalArgumentException("width and height must be > 0");
-    }
-    Bitmap scaledBitmap = ReflectionHelpers.callConstructor(Bitmap.class);
-    ShadowBitmap shadowBitmap = Shadow.extract(scaledBitmap);
-
-    ShadowBitmap shadowSrcBitmap = Shadow.extract(src);
-    shadowBitmap.appendDescription(shadowSrcBitmap.getDescription());
-    shadowBitmap.appendDescription(" scaled to " + dstWidth + " x " + dstHeight);
-    if (filter) {
-      shadowBitmap.appendDescription(" with filter " + filter);
-    }
-
-    shadowBitmap.createdFromBitmap = src;
-    shadowBitmap.scaledFromBitmap = src;
-    shadowBitmap.createdFromFilter = filter;
-    shadowBitmap.width = dstWidth;
-    shadowBitmap.height = dstHeight;
-    shadowBitmap.config = src.getConfig();
-    shadowBitmap.mutable = true;
-    if (!ImageUtil.scaledBitmap(src, scaledBitmap, filter)) {
-      shadowBitmap.bufferedImage =
-          new BufferedImage(dstWidth, dstHeight, BufferedImage.TYPE_INT_ARGB);
-      shadowBitmap.setPixelsInternal(
-          new int[shadowBitmap.getHeight() * shadowBitmap.getWidth()],
-          0,
-          0,
-          0,
-          0,
-          shadowBitmap.getWidth(),
-          shadowBitmap.getHeight());
-    }
-    if (RuntimeEnvironment.getApiLevel() >= O) {
-      shadowBitmap.colorSpace = shadowSrcBitmap.colorSpace;
-    }
-    return scaledBitmap;
-  }
-
-  @Implementation
-  protected static Bitmap nativeCreateFromParcel(Parcel p) {
-    int parceledWidth = p.readInt();
-    int parceledHeight = p.readInt();
-    Bitmap.Config parceledConfig = (Bitmap.Config) p.readSerializable();
-
-    int[] parceledColors = new int[parceledHeight * parceledWidth];
-    p.readIntArray(parceledColors);
-
-    return createBitmap(
-        parceledColors, 0, parceledWidth, parceledWidth, parceledHeight, parceledConfig);
-  }
-
-  static int getBytesPerPixel(Bitmap.Config config) {
-    if (config == null) {
-      throw new NullPointerException("Bitmap config was null.");
-    }
-    switch (config) {
-      case RGBA_F16:
-        return 8;
-      case ARGB_8888:
-      case HARDWARE:
-        return 4;
-      case RGB_565:
-      case ARGB_4444:
-        return 2;
-      case ALPHA_8:
-        return 1;
-      default:
-        throw new IllegalArgumentException("Unknown bitmap config: " + config);
-    }
-  }
-
   /**
    * Reference to original Bitmap from which this Bitmap was created. {@code null} if this Bitmap
    * was not copied from another instance.
    *
    * @return Original Bitmap from which this Bitmap was created.
    */
-  public Bitmap getCreatedFromBitmap() {
-    return createdFromBitmap;
-  }
+  public abstract Bitmap getCreatedFromBitmap();
 
   /**
    * Resource ID from which this Bitmap was created. {@code 0} if this Bitmap was not created from a
@@ -352,9 +36,7 @@
    *
    * @return Resource ID from which this Bitmap was created.
    */
-  public int getCreatedFromResId() {
-    return createdFromResId;
-  }
+  public abstract int getCreatedFromResId();
 
   /**
    * Path from which this Bitmap was created. {@code null} if this Bitmap was not create from a
@@ -362,9 +44,7 @@
    *
    * @return Path from which this Bitmap was created.
    */
-  public String getCreatedFromPath() {
-    return createdFromPath;
-  }
+  public abstract String getCreatedFromPath();
 
   /**
    * {@link InputStream} from which this Bitmap was created. {@code null} if this Bitmap was not
@@ -372,9 +52,7 @@
    *
    * @return InputStream from which this Bitmap was created.
    */
-  public InputStream getCreatedFromStream() {
-    return createdFromStream;
-  }
+  public abstract InputStream getCreatedFromStream();
 
   /**
    * Bytes from which this Bitmap was created. {@code null} if this Bitmap was not created from
@@ -382,27 +60,21 @@
    *
    * @return Bytes from which this Bitmap was created.
    */
-  public byte[] getCreatedFromBytes() {
-    return createdFromBytes;
-  }
+  public abstract byte[] getCreatedFromBytes();
 
   /**
    * Horizontal offset within {@link #getCreatedFromBitmap()} of this Bitmap's content, or -1.
    *
    * @return Horizontal offset within {@link #getCreatedFromBitmap()}.
    */
-  public int getCreatedFromX() {
-    return createdFromX;
-  }
+  public abstract int getCreatedFromX();
 
   /**
    * Vertical offset within {@link #getCreatedFromBitmap()} of this Bitmap's content, or -1.
    *
    * @return Vertical offset within {@link #getCreatedFromBitmap()} of this Bitmap's content, or -1.
    */
-  public int getCreatedFromY() {
-    return createdFromY;
-  }
+  public abstract int getCreatedFromY();
 
   /**
    * Width from {@link #getCreatedFromX()} within {@link #getCreatedFromBitmap()} of this Bitmap's
@@ -411,9 +83,7 @@
    * @return Width from {@link #getCreatedFromX()} within {@link #getCreatedFromBitmap()} of this
    *     Bitmap's content, or -1.
    */
-  public int getCreatedFromWidth() {
-    return createdFromWidth;
-  }
+  public abstract int getCreatedFromWidth();
 
   /**
    * Height from {@link #getCreatedFromX()} within {@link #getCreatedFromBitmap()} of this Bitmap's
@@ -422,9 +92,7 @@
    * @return Height from {@link #getCreatedFromX()} within {@link #getCreatedFromBitmap()} of this
    *     Bitmap's content, or -1.
    */
-  public int getCreatedFromHeight() {
-    return createdFromHeight;
-  }
+  public abstract int getCreatedFromHeight();
 
   /**
    * Color array from which this Bitmap was created. {@code null} if this Bitmap was not created
@@ -432,487 +100,34 @@
    *
    * @return Color array from which this Bitmap was created.
    */
-  public int[] getCreatedFromColors() {
-    return createdFromColors;
-  }
+  public abstract int[] getCreatedFromColors();
 
   /**
    * Matrix from which this Bitmap's content was transformed, or {@code null}.
    *
    * @return Matrix from which this Bitmap's content was transformed, or {@code null}.
    */
-  public Matrix getCreatedFromMatrix() {
-    return createdFromMatrix;
-  }
+  public abstract Matrix getCreatedFromMatrix();
 
   /**
    * {@code true} if this Bitmap was created with filtering.
    *
    * @return {@code true} if this Bitmap was created with filtering.
    */
-  public boolean getCreatedFromFilter() {
-    return createdFromFilter;
-  }
+  public abstract boolean getCreatedFromFilter();
 
-  @Implementation(minSdk = S)
-  public Bitmap asShared() {
-    setMutable(false);
-    return realBitmap;
-  }
+  public abstract void setMutable(boolean mutable);
 
-  @Implementation
-  protected boolean compress(Bitmap.CompressFormat format, int quality, OutputStream stream) {
-    appendDescription(" compressed as " + format + " with quality " + quality);
-    return ImageUtil.writeToStream(realBitmap, format, quality, stream);
-  }
+  public abstract void appendDescription(String s);
 
-  @Implementation
-  protected void setPixels(
-      int[] pixels, int offset, int stride, int x, int y, int width, int height) {
-    checkBitmapMutable();
-    setPixelsInternal(pixels, offset, stride, x, y, width, height);
-  }
+  public abstract String getDescription();
 
-  void setPixelsInternal(
-      int[] pixels, int offset, int stride, int x, int y, int width, int height) {
-    if (bufferedImage == null) {
-      bufferedImage = new BufferedImage(getWidth(), getHeight(), BufferedImage.TYPE_INT_ARGB);
+  public abstract void setDescription(String s);
+
+  /** Shadow picker for {@link Bitmap}. */
+  public static final class Picker extends GraphicsShadowPicker<Object> {
+    public Picker() {
+      super(ShadowLegacyBitmap.class, ShadowNativeBitmap.class);
     }
-    bufferedImage.setRGB(x, y, width, height, pixels, offset, stride);
-  }
-
-  @Implementation
-  protected int getPixel(int x, int y) {
-    internalCheckPixelAccess(x, y);
-    if (bufferedImage != null) {
-      // Note that getPixel() returns a non-premultiplied ARGB value; if
-      // config is RGB_565, our return value will likely be more precise than
-      // on a physical device, since it needs to map each color component from
-      // 5 or 6 bits to 8 bits.
-      return bufferedImage.getRGB(x, y);
-    } else {
-      return 0;
-    }
-  }
-
-  @Implementation
-  protected void setPixel(int x, int y, int color) {
-    checkBitmapMutable();
-    internalCheckPixelAccess(x, y);
-    if (bufferedImage == null) {
-      bufferedImage = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
-    }
-    bufferedImage.setRGB(x, y, color);
-  }
-
-  /**
-   * Note that this method will return a RuntimeException unless: - {@code pixels} has the same
-   * length as the number of pixels of the bitmap. - {@code x = 0} - {@code y = 0} - {@code width}
-   * and {@code height} height match the current bitmap's dimensions.
-   */
-  @Implementation
-  protected void getPixels(
-      int[] pixels, int offset, int stride, int x, int y, int width, int height) {
-    bufferedImage.getRGB(x, y, width, height, pixels, offset, stride);
-  }
-
-  @Implementation
-  protected int getRowBytes() {
-    return getBytesPerPixel(config) * getWidth();
-  }
-
-  @Implementation
-  protected int getByteCount() {
-    return getRowBytes() * getHeight();
-  }
-
-  @Implementation
-  protected void recycle() {
-    recycled = true;
-  }
-
-  @Implementation
-  protected final boolean isRecycled() {
-    return recycled;
-  }
-
-  @Implementation
-  protected Bitmap copy(Bitmap.Config config, boolean isMutable) {
-    Bitmap newBitmap = ReflectionHelpers.callConstructor(Bitmap.class);
-    ShadowBitmap shadowBitmap = Shadow.extract(newBitmap);
-    shadowBitmap.createdFromBitmap = realBitmap;
-    shadowBitmap.config = config;
-    shadowBitmap.mutable = isMutable;
-    shadowBitmap.height = getHeight();
-    shadowBitmap.width = getWidth();
-    if (bufferedImage != null) {
-      ColorModel cm = bufferedImage.getColorModel();
-      WritableRaster raster =
-          bufferedImage.copyData(bufferedImage.getRaster().createCompatibleWritableRaster());
-      shadowBitmap.bufferedImage = new BufferedImage(cm, raster, false, null);
-    }
-    return newBitmap;
-  }
-
-  @Implementation(minSdk = KITKAT)
-  protected final int getAllocationByteCount() {
-    return getRowBytes() * getHeight();
-  }
-
-  @Implementation
-  protected final Bitmap.Config getConfig() {
-    return config;
-  }
-
-  @Implementation(minSdk = KITKAT)
-  protected void setConfig(Bitmap.Config config) {
-    this.config = config;
-  }
-
-  @Implementation
-  protected final boolean isMutable() {
-    return mutable;
-  }
-
-  public void setMutable(boolean mutable) {
-    this.mutable = mutable;
-  }
-
-  public void appendDescription(String s) {
-    description += s;
-  }
-
-  public String getDescription() {
-    return description;
-  }
-
-  public void setDescription(String s) {
-    description = s;
-  }
-
-  @Implementation
-  protected final boolean hasAlpha() {
-    return hasAlpha && config != Bitmap.Config.RGB_565;
-  }
-
-  @Implementation
-  protected void setHasAlpha(boolean hasAlpha) {
-    this.hasAlpha = hasAlpha;
-  }
-
-  @Implementation
-  protected Bitmap extractAlpha() {
-    WritableRaster raster = bufferedImage.getAlphaRaster();
-    BufferedImage alphaImage = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
-    alphaImage.getAlphaRaster().setRect(raster);
-    return createBitmap(alphaImage, getWidth(), getHeight(), Bitmap.Config.ALPHA_8);
-  }
-
-  /**
-   * This shadow implementation ignores the given paint and offsetXY and simply calls {@link
-   * #extractAlpha()}.
-   */
-  @Implementation
-  protected Bitmap extractAlpha(Paint paint, int[] offsetXY) {
-    return extractAlpha();
-  }
-
-  @Implementation(minSdk = JELLY_BEAN_MR1)
-  protected final boolean hasMipMap() {
-    return hasMipMap;
-  }
-
-  @Implementation(minSdk = JELLY_BEAN_MR1)
-  protected final void setHasMipMap(boolean hasMipMap) {
-    this.hasMipMap = hasMipMap;
-  }
-
-  @Implementation
-  protected int getWidth() {
-    return width;
-  }
-
-  @Implementation(minSdk = KITKAT)
-  protected void setWidth(int width) {
-    this.width = width;
-  }
-
-  @Implementation
-  protected int getHeight() {
-    return height;
-  }
-
-  @Implementation(minSdk = KITKAT)
-  protected void setHeight(int height) {
-    this.height = height;
-  }
-
-  @Implementation
-  protected int getGenerationId() {
-    return 0;
-  }
-
-  @Implementation(minSdk = M)
-  protected Bitmap createAshmemBitmap() {
-    return realBitmap;
-  }
-
-  @Implementation
-  protected void eraseColor(int color) {
-    if (bufferedImage != null) {
-      int[] pixels = ((DataBufferInt) bufferedImage.getRaster().getDataBuffer()).getData();
-      Arrays.fill(pixels, color);
-    }
-    setDescription(String.format("Bitmap (%d, %d)", width, height));
-    if (color != 0) {
-      appendDescription(String.format(" erased with 0x%08x", color));
-    }
-  }
-
-  @Implementation
-  protected void writeToParcel(Parcel p, int flags) {
-    p.writeInt(width);
-    p.writeInt(height);
-    p.writeSerializable(config);
-    int[] pixels = new int[width * height];
-    getPixels(pixels, 0, width, 0, 0, width, height);
-    p.writeIntArray(pixels);
-  }
-
-  @Implementation
-  protected void copyPixelsFromBuffer(Buffer dst) {
-    if (isRecycled()) {
-      throw new IllegalStateException("Can't call copyPixelsFromBuffer() on a recycled bitmap");
-    }
-
-    // See the related comment in #copyPixelsToBuffer(Buffer).
-    if (getBytesPerPixel(config) != INTERNAL_BYTES_PER_PIXEL) {
-      throw new RuntimeException(
-          "Not implemented: only Bitmaps with "
-              + INTERNAL_BYTES_PER_PIXEL
-              + " bytes per pixel are supported");
-    }
-    if (!(dst instanceof ByteBuffer) && !(dst instanceof IntBuffer)) {
-      throw new RuntimeException("Not implemented: unsupported Buffer subclass");
-    }
-
-    ByteBuffer byteBuffer = null;
-    IntBuffer intBuffer;
-    if (dst instanceof IntBuffer) {
-      intBuffer = (IntBuffer) dst;
-    } else {
-      byteBuffer = (ByteBuffer) dst;
-      intBuffer = byteBuffer.asIntBuffer();
-    }
-
-    if (intBuffer.remaining() < (width * height)) {
-      throw new RuntimeException("Buffer not large enough for pixels");
-    }
-
-    int[] colors = new int[width * height];
-    intBuffer.get(colors);
-    if (byteBuffer != null) {
-      byteBuffer.position(byteBuffer.position() + intBuffer.position() * INTERNAL_BYTES_PER_PIXEL);
-    }
-    int[] pixels = ((DataBufferInt) bufferedImage.getRaster().getDataBuffer()).getData();
-    System.arraycopy(colors, 0, pixels, 0, pixels.length);
-  }
-
-  @Implementation
-  protected void copyPixelsToBuffer(Buffer dst) {
-    // Ensure that the Bitmap uses 4 bytes per pixel, since we always use 4 bytes per pixels
-    // internally. Clients of this API probably expect that the buffer size must be >=
-    // getByteCount(), but if we don't enforce this restriction then for RGB_4444 and other
-    // configs that value would be smaller then the buffer size we actually need.
-    if (getBytesPerPixel(config) != INTERNAL_BYTES_PER_PIXEL) {
-      throw new RuntimeException(
-          "Not implemented: only Bitmaps with "
-              + INTERNAL_BYTES_PER_PIXEL
-              + " bytes per pixel are supported");
-    }
-
-    if (!(dst instanceof ByteBuffer) && !(dst instanceof IntBuffer)) {
-      throw new RuntimeException("Not implemented: unsupported Buffer subclass");
-    }
-    int[] pixels = ((DataBufferInt) bufferedImage.getRaster().getDataBuffer()).getData();
-    if (dst instanceof ByteBuffer) {
-      IntBuffer intBuffer = ((ByteBuffer) dst).asIntBuffer();
-      intBuffer.put(pixels);
-      dst.position(intBuffer.position() * 4);
-    } else if (dst instanceof IntBuffer) {
-      ((IntBuffer) dst).put(pixels);
-    }
-  }
-
-  @Implementation(minSdk = KITKAT)
-  protected void reconfigure(int width, int height, Bitmap.Config config) {
-    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && this.config == Bitmap.Config.HARDWARE) {
-      throw new IllegalStateException("native-backed bitmaps may not be reconfigured");
-    }
-
-    // This should throw if the resulting allocation size is greater than the initial allocation
-    // size of our Bitmap, but we don't keep track of that information reliably, so we're forced to
-    // assume that our original dimensions and config are large enough to fit the new dimensions and
-    // config
-    this.width = width;
-    this.height = height;
-    this.config = config;
-    bufferedImage = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
-  }
-
-  @Implementation(minSdk = KITKAT)
-  protected boolean isPremultiplied() {
-    return requestPremultiplied && hasAlpha();
-  }
-
-  @Implementation(minSdk = KITKAT)
-  protected void setPremultiplied(boolean isPremultiplied) {
-    this.requestPremultiplied = isPremultiplied;
-  }
-
-  @Implementation(minSdk = O)
-  protected ColorSpace getColorSpace() {
-    return colorSpace;
-  }
-
-  @Implementation(minSdk = Q)
-  protected void setColorSpace(ColorSpace colorSpace) {
-    this.colorSpace = checkNotNull(colorSpace);
-  }
-
-  @Implementation
-  protected boolean sameAs(Bitmap other) {
-    if (other == null) {
-      return false;
-    }
-    ShadowBitmap shadowOtherBitmap = Shadow.extract(other);
-    if (this.width != shadowOtherBitmap.width || this.height != shadowOtherBitmap.height) {
-      return false;
-    }
-    if (this.config != shadowOtherBitmap.config) {
-      return false;
-    }
-
-    if (bufferedImage == null && shadowOtherBitmap.bufferedImage != null) {
-      return false;
-    } else if (bufferedImage != null && shadowOtherBitmap.bufferedImage == null) {
-      return false;
-    } else if (bufferedImage != null && shadowOtherBitmap.bufferedImage != null) {
-      int[] pixels = ((DataBufferInt) bufferedImage.getData().getDataBuffer()).getData();
-      int[] otherPixels =
-          ((DataBufferInt) shadowOtherBitmap.bufferedImage.getData().getDataBuffer()).getData();
-      if (!Arrays.equals(pixels, otherPixels)) {
-        return false;
-      }
-    }
-    // When Bitmap.createScaledBitmap is called, the colors array is cleared, so we need a basic
-    // way to detect if two scaled bitmaps are the same.
-    if (scaledFromBitmap != null && shadowOtherBitmap.scaledFromBitmap != null) {
-      return scaledFromBitmap.sameAs(shadowOtherBitmap.scaledFromBitmap);
-    }
-    return true;
-  }
-
-  public void setCreatedFromResId(int resId, String description) {
-    this.createdFromResId = resId;
-    appendDescription(" for resource:" + description);
-  }
-
-  private void checkBitmapMutable() {
-    if (isRecycled()) {
-      throw new IllegalStateException("Can't call setPixel() on a recycled bitmap");
-    } else if (!isMutable()) {
-      throw new IllegalStateException("Bitmap is immutable");
-    }
-  }
-
-  private void internalCheckPixelAccess(int x, int y) {
-    if (x < 0) {
-      throw new IllegalArgumentException("x must be >= 0");
-    }
-    if (y < 0) {
-      throw new IllegalArgumentException("y must be >= 0");
-    }
-    if (x >= getWidth()) {
-      throw new IllegalArgumentException("x must be < bitmap.width()");
-    }
-    if (y >= getHeight()) {
-      throw new IllegalArgumentException("y must be < bitmap.height()");
-    }
-  }
-
-  void drawRect(Rect r, Paint paint) {
-    if (bufferedImage == null) {
-      return;
-    }
-    int[] pixels = ((DataBufferInt) bufferedImage.getRaster().getDataBuffer()).getData();
-
-    Rect toDraw =
-        new Rect(
-            max(0, r.left), max(0, r.top), min(getWidth(), r.right), min(getHeight(), r.bottom));
-    if (toDraw.left == 0 && toDraw.top == 0 && toDraw.right == getWidth()) {
-      Arrays.fill(pixels, 0, getWidth() * toDraw.bottom, paint.getColor());
-      return;
-    }
-    for (int y = toDraw.top; y < toDraw.bottom; y++) {
-      Arrays.fill(
-          pixels, y * getWidth() + toDraw.left, y * getWidth() + toDraw.right, paint.getColor());
-    }
-  }
-
-  void drawRect(RectF r, Paint paint) {
-    if (bufferedImage == null) {
-      return;
-    }
-
-    Graphics2D graphics2D = bufferedImage.createGraphics();
-    Rectangle2D r2d = new Rectangle2D.Float(r.left, r.top, r.right - r.left, r.bottom - r.top);
-    graphics2D.setColor(new Color(paint.getColor()));
-    graphics2D.draw(r2d);
-    graphics2D.dispose();
-  }
-
-  void drawBitmap(Bitmap source, int left, int top) {
-    ShadowBitmap shadowSource = Shadows.shadowOf(source);
-    if (bufferedImage == null || shadowSource.bufferedImage == null) {
-      // pixel data not available, so there's nothing we can do
-      return;
-    }
-
-    int[] pixels = ((DataBufferInt) bufferedImage.getRaster().getDataBuffer()).getData();
-    int[] sourcePixels =
-        ((DataBufferInt) shadowSource.bufferedImage.getRaster().getDataBuffer()).getData();
-
-    // fast path
-    if (left == 0 && top == 0 && getWidth() == source.getWidth()) {
-      int size = min(getWidth() * getHeight(), source.getWidth() * source.getHeight());
-      System.arraycopy(sourcePixels, 0, pixels, 0, size);
-      return;
-    }
-    // slower (row-by-row) path
-    int startSourceY = max(0, -top);
-    int startSourceX = max(0, -left);
-    int startY = max(0, top);
-    int startX = max(0, left);
-    int endY = min(getHeight(), top + source.getHeight());
-    int endX = min(getWidth(), left + source.getWidth());
-    int lenY = endY - startY;
-    int lenX = endX - startX;
-    for (int y = 0; y < lenY; y++) {
-      System.arraycopy(
-          sourcePixels,
-          (startSourceY + y) * source.getWidth() + startSourceX,
-          pixels,
-          (startY + y) * getWidth() + startX,
-          lenX);
-    }
-  }
-
-  BufferedImage getBufferedImage() {
-    return bufferedImage;
-  }
-
-  void setBufferedImage(BufferedImage bufferedImage) {
-    this.bufferedImage = bufferedImage;
   }
 }
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowBitmapDrawable.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowBitmapDrawable.java
index de499e9..3bdf7ab 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowBitmapDrawable.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowBitmapDrawable.java
@@ -42,8 +42,8 @@
   protected void setCreatedFromResId(int createdFromResId, String resourceName) {
     super.setCreatedFromResId(createdFromResId, resourceName);
     Bitmap bitmap = realBitmapDrawable.getBitmap();
-    if (bitmap != null && Shadow.extract(bitmap) instanceof ShadowBitmap) {
-      ShadowBitmap shadowBitmap = Shadow.extract(bitmap);
+    if (bitmap != null && Shadow.extract(bitmap) instanceof ShadowLegacyBitmap) {
+      ShadowLegacyBitmap shadowBitmap = Shadow.extract(bitmap);
       if (shadowBitmap.createdFromResId == -1) {
         shadowBitmap.setCreatedFromResId(createdFromResId, resourceName);
       }
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowBitmapFactory.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowBitmapFactory.java
index 33bc8fd..fd2c084 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowBitmapFactory.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowBitmapFactory.java
@@ -85,7 +85,7 @@
       return null;
     }
     Bitmap bitmap = create("resource:" + resourceName, options, image);
-    ShadowBitmap shadowBitmap = Shadow.extract(bitmap);
+    ShadowLegacyBitmap shadowBitmap = Shadow.extract(bitmap);
     shadowBitmap.createdFromResId = id;
     return bitmap;
   }
@@ -116,7 +116,7 @@
       return null;
     }
     Bitmap bitmap = create("file:" + pathName, options, image);
-    ShadowBitmap shadowBitmap = Shadow.extract(bitmap);
+    ShadowLegacyBitmap shadowBitmap = Shadow.extract(bitmap);
     shadowBitmap.createdFromPath = pathName;
     return bitmap;
   }
@@ -143,7 +143,7 @@
       return null;
     }
     Bitmap bitmap = create("fd:" + fd, null, outPadding, opts, null, image);
-    ShadowBitmap shadowBitmap = Shadow.extract(bitmap);
+    ShadowLegacyBitmap shadowBitmap = Shadow.extract(bitmap);
     shadowBitmap.createdFromFileDescriptor = fd;
     return bitmap;
   }
@@ -189,7 +189,7 @@
     Bitmap bitmap = create(name, null, outPadding, opts, null, image);
     ReflectionHelpers.callInstanceMethod(
         bitmap, "setNinePatchChunk", ClassParameter.from(byte[].class, ninePatchChunk));
-    ShadowBitmap shadowBitmap = Shadow.extract(bitmap);
+    ShadowLegacyBitmap shadowBitmap = Shadow.extract(bitmap);
     shadowBitmap.createdFromStream = is;
 
     if (image != null && opts != null) {
@@ -222,7 +222,7 @@
       return null;
     }
     Bitmap bitmap = create(desc, data, null, opts, null, image);
-    ShadowBitmap shadowBitmap = Shadow.extract(bitmap);
+    ShadowLegacyBitmap shadowBitmap = Shadow.extract(bitmap);
     shadowBitmap.createdFromBytes = data;
     return bitmap;
   }
@@ -242,7 +242,7 @@
       final Point widthAndHeightOverride,
       final RobolectricBufferedImage image) {
     Bitmap bitmap = Shadow.newInstanceOf(Bitmap.class);
-    ShadowBitmap shadowBitmap = Shadow.extract(bitmap);
+    ShadowLegacyBitmap shadowBitmap = Shadow.extract(bitmap);
     shadowBitmap.appendDescription(name == null ? "Bitmap" : "Bitmap for " + name);
 
     Bitmap.Config config;
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowBluetoothAdapter.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowBluetoothAdapter.java
index 8634585..f872506 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowBluetoothAdapter.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowBluetoothAdapter.java
@@ -30,11 +30,14 @@
 import android.os.Build.VERSION_CODES;
 import android.os.ParcelUuid;
 import android.provider.Settings;
+import com.google.common.collect.ImmutableList;
 import java.io.IOException;
 import java.time.Duration;
+import java.util.ArrayList;
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.HashSet;
+import java.util.List;
 import java.util.Map;
 import java.util.Set;
 import java.util.UUID;
@@ -99,6 +102,8 @@
   private final Map<Integer, BluetoothProfile> profileProxies = new HashMap<>();
   private final ConcurrentMap<UUID, BackgroundRfcommServerEntry> backgroundRfcommServers =
       new ConcurrentHashMap<>();
+  private final Map<Integer, List<BluetoothProfile.ServiceListener>>
+      bluetoothProfileServiceListeners = new HashMap<>();
 
   @Resetter
   public static void reset() {
@@ -528,6 +533,13 @@
       return false;
     } else {
       listener.onServiceConnected(profile, proxy);
+      List<BluetoothProfile.ServiceListener> profileListeners =
+          bluetoothProfileServiceListeners.get(profile);
+      if (profileListeners != null) {
+        profileListeners.add(listener);
+      } else {
+        bluetoothProfileServiceListeners.put(profile, new ArrayList<>(ImmutableList.of(listener)));
+      }
       return true;
     }
   }
@@ -548,6 +560,13 @@
 
     if (proxy != null && proxy.equals(profileProxies.get(profile))) {
       profileProxies.remove(profile);
+      List<BluetoothProfile.ServiceListener> profileListeners =
+          bluetoothProfileServiceListeners.remove(profile);
+      if (profileListeners != null) {
+        for (BluetoothProfile.ServiceListener listener : profileListeners) {
+          listener.onServiceDisconnected(profile);
+        }
+      }
     }
   }
 
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowBluetoothDevice.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowBluetoothDevice.java
index 95c7547..d7ed1ca 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowBluetoothDevice.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowBluetoothDevice.java
@@ -7,6 +7,7 @@
 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 android.os.Build.VERSION_CODES.S;
 import static org.robolectric.util.reflector.Reflector.reflector;
 
 import android.annotation.IntRange;
@@ -16,10 +17,10 @@
 import android.bluetooth.BluetoothGatt;
 import android.bluetooth.BluetoothGattCallback;
 import android.bluetooth.BluetoothSocket;
+import android.bluetooth.BluetoothStatusCodes;
 import android.bluetooth.IBluetooth;
 import android.content.Context;
 import android.os.Build.VERSION;
-import android.os.Build.VERSION_CODES;
 import android.os.Handler;
 import android.os.ParcelUuid;
 import java.io.IOException;
@@ -39,7 +40,8 @@
 import org.robolectric.util.reflector.ForType;
 import org.robolectric.util.reflector.Static;
 
-@Implements(BluetoothDevice.class)
+/** Shadow for {@link BluetoothDevice}. */
+@Implements(value = BluetoothDevice.class, looseSignatures = true)
 public class ShadowBluetoothDevice {
   @Deprecated // Prefer {@link android.bluetooth.BluetoothAdapter#getRemoteDevice}
   public static BluetoothDevice newInstance(String address) {
@@ -103,8 +105,14 @@
    *
    * @param alias alias name.
    */
-  public void setAlias(String alias) {
-    this.alias = alias;
+  @Implementation
+  public Object setAlias(Object alias) {
+    this.alias = (String) alias;
+    if (RuntimeEnvironment.getApiLevel() >= S) {
+      return BluetoothStatusCodes.SUCCESS;
+    } else {
+      return true;
+    }
   }
 
   /**
@@ -438,7 +446,7 @@
 
   private void checkForBluetoothConnectPermission() {
     if (shouldThrowSecurityExceptions
-        && VERSION.SDK_INT >= VERSION_CODES.S
+        && VERSION.SDK_INT >= S
         && !checkPermission(android.Manifest.permission.BLUETOOTH_CONNECT)) {
       throw new SecurityException("Bluetooth connect permission required.");
     }
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowBluetoothGatt.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowBluetoothGatt.java
index 5f5fd88..737df1f 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowBluetoothGatt.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowBluetoothGatt.java
@@ -10,15 +10,41 @@
 import android.bluetooth.BluetoothDevice;
 import android.bluetooth.BluetoothGatt;
 import android.bluetooth.BluetoothGattCallback;
+import android.bluetooth.BluetoothGattCharacteristic;
+import android.bluetooth.BluetoothGattService;
+import android.bluetooth.BluetoothProfile;
 import android.content.Context;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
 import org.robolectric.RuntimeEnvironment;
 import org.robolectric.annotation.Implementation;
 import org.robolectric.annotation.Implements;
+import org.robolectric.annotation.RealObject;
+import org.robolectric.annotation.ReflectorObject;
 import org.robolectric.shadow.api.Shadow;
+import org.robolectric.util.PerfStatsCollector;
+import org.robolectric.util.reflector.Direct;
+import org.robolectric.util.reflector.ForType;
 
+/** Shadow implementation of {@link BluetoothGatt}. */
 @Implements(value = BluetoothGatt.class, minSdk = JELLY_BEAN_MR2)
 public class ShadowBluetoothGatt {
+
+  private static final String NULL_CALLBACK_MSG = "BluetoothGattCallback can not be null.";
+
   private BluetoothGattCallback bluetoothGattCallback;
+  private int connectionPriority = BluetoothGatt.CONNECTION_PRIORITY_BALANCED;
+  private boolean isConnected = false;
+  private boolean isClosed = false;
+  private byte[] writtenBytes;
+  private byte[] readBytes;
+  private final Set<BluetoothGattService> discoverableServices = new HashSet<>();
+  private final ArrayList<BluetoothGattService> services = new ArrayList<>();
+
+  @RealObject private BluetoothGatt realBluetoothGatt;
+  @ReflectorObject protected BluetoothGattReflector bluetoothGattReflector;
 
   @SuppressLint("PrivateApi")
   @SuppressWarnings("unchecked")
@@ -77,27 +103,204 @@
                 new Class<?>[] {Context.class, iBluetoothGattClass, BluetoothDevice.class},
                 new Object[] {RuntimeEnvironment.getApplication(), null, device});
       }
+
+      PerfStatsCollector.getInstance().incrementCount("constructShadowBluetoothGatt");
       return bluetoothGatt;
     } catch (ClassNotFoundException e) {
       throw new RuntimeException(e);
     }
   }
 
-  /* package */ BluetoothGattCallback getGattCallback() {
-    return bluetoothGattCallback;
-  }
-
-  /* package */ void setGattCallback(BluetoothGattCallback bluetoothGattCallback) {
-    this.bluetoothGattCallback = bluetoothGattCallback;
-  }
-
   /**
-   * Overrides behavior of {@link BluetoothGatt#connect()} to always return true.
+   * Connect to a remote device, and performs a {@link
+   * BluetoothGattCallback#onConnectionStateChange} if a {@link BluetoothGattCallback} has been set
+   * by {@link ShadowBluetoothGatt#setGattCallback}
    *
-   * @return true, unconditionally
+   * @return true, if a {@link BluetoothGattCallback} has been set by {@link
+   *     ShadowBluetoothGatt#setGattCallback}
    */
   @Implementation(minSdk = JELLY_BEAN_MR2)
   protected boolean connect() {
+    if (this.getGattCallback() != null) {
+      this.isConnected = true;
+      this.getGattCallback()
+          .onConnectionStateChange(
+              this.realBluetoothGatt, BluetoothGatt.GATT_SUCCESS, BluetoothProfile.STATE_CONNECTED);
+      return true;
+    }
+    return false;
+  }
+
+  /**
+   * Disconnects an established connection, or cancels a connection attempt currently in progress.
+   */
+  @Implementation(minSdk = JELLY_BEAN_MR2)
+  protected void disconnect() {
+    bluetoothGattReflector.disconnect();
+    if (this.getGattCallback() != null && this.isConnected) {
+      this.getGattCallback()
+          .onConnectionStateChange(
+              this.realBluetoothGatt,
+              BluetoothGatt.GATT_SUCCESS,
+              BluetoothProfile.STATE_DISCONNECTED);
+    }
+    this.isConnected = false;
+  }
+
+  /** Close this Bluetooth GATT client. */
+  @Implementation(minSdk = JELLY_BEAN_MR2)
+  protected void close() {
+    bluetoothGattReflector.close();
+    this.isClosed = true;
+    this.isConnected = false;
+  }
+
+  /**
+   * Request a connection parameter update.
+   *
+   * @param priority Request a specific connection priority. Must be one of {@link
+   *     BluetoothGatt#CONNECTION_PRIORITY_BALANCED}, {@link BluetoothGatt#CONNECTION_PRIORITY_HIGH}
+   *     or {@link BluetoothGatt#CONNECTION_PRIORITY_LOW_POWER}.
+   * @return true if operation is successful.
+   * @throws IllegalArgumentException If the parameters are outside of their specified range.
+   */
+  @Implementation(minSdk = O)
+  protected boolean requestConnectionPriority(int priority) {
+    if (priority == BluetoothGatt.CONNECTION_PRIORITY_HIGH
+        || priority == BluetoothGatt.CONNECTION_PRIORITY_BALANCED
+        || priority == BluetoothGatt.CONNECTION_PRIORITY_LOW_POWER) {
+      this.connectionPriority = priority;
+      return true;
+    }
+    throw new IllegalArgumentException("connection priority not within valid range");
+  }
+
+  /**
+   * Overrides {@link BluetoothGatt#discoverServices} to always return false unless there are
+   * discoverable services made available by {@link ShadowBluetoothGatt#addDiscoverableService}
+   *
+   * @return true if discoverable service is available and callback response is possible
+   */
+  @Implementation(minSdk = O)
+  protected boolean discoverServices() {
+    this.services.clear();
+    if (!this.discoverableServices.isEmpty()) {
+      this.services.addAll(this.discoverableServices);
+
+      if (this.getGattCallback() != null) {
+        this.getGattCallback()
+            .onServicesDiscovered(this.realBluetoothGatt, BluetoothGatt.GATT_SUCCESS);
+        return true;
+      }
+    }
+    return false;
+  }
+
+  /**
+   * Overrides {@link BluetoothGatt#getServices} to always return a list of services discovered.
+   *
+   * @return list of services that have been discovered through {@link
+   *     ShadowBluetoothGatt#discoverServices}, empty if none.
+   */
+  @Implementation(minSdk = O)
+  protected List<BluetoothGattService> getServices() {
+    return new ArrayList<>(this.services);
+  }
+
+  /**
+   * Reads bytes from incoming characteristic if properties are valid and callback is set. Callback
+   * responds with {@link BluetoothGattCallback#onCharacteristicWrite} and returns true when
+   * successful.
+   *
+   * @param characteristic Characteristic to read
+   * @return true, if the read operation was initiated successfully
+   * @throws IllegalStateException if a {@link BluetoothGattCallback} has not been set by {@link
+   *     ShadowBluetoothGatt#setGattCallback}
+   */
+  public boolean writeIncomingCharacteristic(BluetoothGattCharacteristic characteristic) {
+    if (this.getGattCallback() == null) {
+      throw new IllegalStateException(NULL_CALLBACK_MSG);
+    }
+    if (characteristic.getService() == null
+        || ((characteristic.getProperties() & BluetoothGattCharacteristic.PROPERTY_WRITE) == 0
+            && (characteristic.getProperties()
+                    & BluetoothGattCharacteristic.PROPERTY_WRITE_NO_RESPONSE)
+                == 0)) {
+      return false;
+    }
+    this.writtenBytes = characteristic.getValue();
+    this.bluetoothGattCallback.onCharacteristicWrite(
+        this.realBluetoothGatt, characteristic, BluetoothGatt.GATT_SUCCESS);
     return true;
   }
+
+  /**
+   * Writes bytes from incoming characteristic if properties are valid and callback is set. Callback
+   * responds with BluetoothGattCallback#onCharacteristicRead and returns true when successful.
+   *
+   * @param characteristic Characteristic to read
+   * @return true, if the read operation was initiated successfully
+   * @throws IllegalStateException if a {@link BluetoothGattCallback} has not been set by {@link
+   *     ShadowBluetoothGatt#setGattCallback}
+   */
+  public boolean readIncomingCharacteristic(BluetoothGattCharacteristic characteristic) {
+    if (this.getGattCallback() == null) {
+      throw new IllegalStateException(NULL_CALLBACK_MSG);
+    }
+    if ((characteristic.getProperties() & BluetoothGattCharacteristic.PROPERTY_READ) == 0
+        || characteristic.getService() == null) {
+      return false;
+    }
+
+    this.readBytes = characteristic.getValue();
+    this.bluetoothGattCallback.onCharacteristicRead(
+        this.realBluetoothGatt, characteristic, BluetoothGatt.GATT_SUCCESS);
+    return true;
+  }
+
+  public void addDiscoverableService(BluetoothGattService service) {
+    this.discoverableServices.add(service);
+  }
+
+  public void removeDiscoverableService(BluetoothGattService service) {
+    this.discoverableServices.remove(service);
+  }
+
+  public BluetoothGattCallback getGattCallback() {
+    return this.bluetoothGattCallback;
+  }
+
+  public void setGattCallback(BluetoothGattCallback bluetoothGattCallback) {
+    this.bluetoothGattCallback = bluetoothGattCallback;
+  }
+
+  public boolean isConnected() {
+    return this.isConnected;
+  }
+
+  public boolean isClosed() {
+    return this.isClosed;
+  }
+
+  public int getConnectionPriority() {
+    return this.connectionPriority;
+  }
+
+  public byte[] getLatestWrittenBytes() {
+    return this.writtenBytes;
+  }
+
+  public byte[] getLatestReadBytes() {
+    return this.readBytes;
+  }
+
+  @ForType(BluetoothGatt.class)
+  private interface BluetoothGattReflector {
+
+    @Direct
+    void disconnect();
+
+    @Direct
+    void close();
+  }
 }
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowBluetoothHeadset.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowBluetoothHeadset.java
index 51dde02..54b9626 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowBluetoothHeadset.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowBluetoothHeadset.java
@@ -2,6 +2,7 @@
 
 import static android.os.Build.VERSION_CODES.KITKAT;
 import static android.os.Build.VERSION_CODES.P;
+import static android.os.Build.VERSION_CODES.S;
 
 import android.bluetooth.BluetoothDevice;
 import android.bluetooth.BluetoothHeadset;
@@ -23,6 +24,7 @@
   private final List<BluetoothDevice> connectedDevices = new ArrayList<>();
   private boolean allowsSendVendorSpecificResultCode = true;
   private BluetoothDevice activeBluetoothDevice;
+  private boolean isVoiceRecognitionSupported = true;
 
   /**
    * Overrides behavior of {@link getConnectedDevices}. Returns list of devices that is set up by
@@ -130,6 +132,27 @@
   }
 
   /**
+   * Sets whether the headset supports voice recognition.
+   *
+   * <p>By default voice recognition is supported.
+   *
+   * @see #isVoiceRecognitionSupported(BluetoothDevice)
+   */
+  public void setVoiceRecognitionSupported(boolean supported) {
+    isVoiceRecognitionSupported = supported;
+  }
+
+  /**
+   * Checks whether the headset supports voice recognition.
+   *
+   * @see #setVoiceRecognitionSupported(boolean)
+   */
+  @Implementation(minSdk = S)
+  protected boolean isVoiceRecognitionSupported(BluetoothDevice device) {
+    return isVoiceRecognitionSupported;
+  }
+
+  /**
    * Affects the behavior of {@link BluetoothHeadset#sendVendorSpecificResultCode}
    *
    * @param allowsSendVendorSpecificResultCode can be set to 'false' to simulate the situation where
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowCamcorderProfile.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowCamcorderProfile.java
new file mode 100644
index 0000000..8576f70
--- /dev/null
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowCamcorderProfile.java
@@ -0,0 +1,66 @@
+package org.robolectric.shadows;
+
+import android.media.CamcorderProfile;
+import com.google.common.collect.HashBasedTable;
+import com.google.common.collect.Table;
+import org.robolectric.annotation.Implementation;
+import org.robolectric.annotation.Implements;
+import org.robolectric.annotation.Resetter;
+import org.robolectric.util.ReflectionHelpers;
+import org.robolectric.util.ReflectionHelpers.ClassParameter;
+
+/** Shadow of the CamcorderProfile that allows the caller to add custom profile settings. */
+@Implements(CamcorderProfile.class)
+public class ShadowCamcorderProfile {
+
+  private static final Table<Integer, Integer, CamcorderProfile> profiles = HashBasedTable.create();
+
+  public static void addProfile(int cameraId, int quality, CamcorderProfile profile) {
+    profiles.put(cameraId, quality, profile);
+  }
+
+  @Resetter
+  public static void reset() {
+    profiles.clear();
+  }
+
+  public static CamcorderProfile createProfile(
+      int duration,
+      int quality,
+      int fileFormat,
+      int videoCodec,
+      int videoBitRate,
+      int videoFrameRate,
+      int videoWidth,
+      int videoHeight,
+      int audioCodec,
+      int audioBitRate,
+      int audioSampleRate,
+      int audioChannels) {
+    // CamcorderProfile doesn't have a public constructor. To construct we need to use reflection.
+    return ReflectionHelpers.callConstructor(
+        CamcorderProfile.class,
+        ClassParameter.from(int.class, duration),
+        ClassParameter.from(int.class, quality),
+        ClassParameter.from(int.class, fileFormat),
+        ClassParameter.from(int.class, videoCodec),
+        ClassParameter.from(int.class, videoBitRate),
+        ClassParameter.from(int.class, videoFrameRate),
+        ClassParameter.from(int.class, videoWidth),
+        ClassParameter.from(int.class, videoHeight),
+        ClassParameter.from(int.class, audioCodec),
+        ClassParameter.from(int.class, audioBitRate),
+        ClassParameter.from(int.class, audioSampleRate),
+        ClassParameter.from(int.class, audioChannels));
+  }
+
+  @Implementation
+  protected static boolean hasProfile(int cameraId, int quality) {
+    return profiles.contains(cameraId, quality);
+  }
+
+  @Implementation
+  protected static CamcorderProfile get(int cameraId, int quality) {
+    return profiles.get(cameraId, quality);
+  }
+}
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowCanvas.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowCanvas.java
index 310b610..f45737b 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowCanvas.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowCanvas.java
@@ -1,582 +1,80 @@
 package org.robolectric.shadows;
 
-import static android.os.Build.VERSION_CODES.KITKAT;
-import static android.os.Build.VERSION_CODES.KITKAT_WATCH;
-import static android.os.Build.VERSION_CODES.LOLLIPOP;
-import static android.os.Build.VERSION_CODES.LOLLIPOP_MR1;
-import static android.os.Build.VERSION_CODES.M;
-import static android.os.Build.VERSION_CODES.N_MR1;
-import static android.os.Build.VERSION_CODES.O;
-import static android.os.Build.VERSION_CODES.P;
-import static android.os.Build.VERSION_CODES.Q;
-import static android.os.Build.VERSION_CODES.R;
-import static android.os.Build.VERSION_CODES.S;
-
-import android.graphics.Bitmap;
 import android.graphics.Canvas;
-import android.graphics.ColorFilter;
-import android.graphics.Matrix;
 import android.graphics.Paint;
 import android.graphics.Path;
-import android.graphics.Rect;
 import android.graphics.RectF;
-import com.google.common.base.Preconditions;
-import java.util.ArrayList;
-import java.util.List;
-import org.robolectric.RuntimeEnvironment;
-import org.robolectric.Shadows;
-import org.robolectric.annotation.Implementation;
 import org.robolectric.annotation.Implements;
-import org.robolectric.annotation.RealObject;
-import org.robolectric.annotation.ReflectorObject;
-import org.robolectric.annotation.Resetter;
-import org.robolectric.res.android.NativeObjRegistry;
 import org.robolectric.shadow.api.Shadow;
-import org.robolectric.util.ReflectionHelpers;
-import org.robolectric.util.reflector.Direct;
-import org.robolectric.util.reflector.ForType;
+import org.robolectric.shadows.ShadowCanvas.Picker;
 
-/**
- * Broken. This implementation is very specific to the application for which it was developed. Todo:
- * Reimplement. Consider using the same strategy of collecting a history of draw events and
- * providing methods for writing queries based on type, number, and order of events.
- */
-@SuppressWarnings({"UnusedDeclaration"})
-@Implements(Canvas.class)
-public class ShadowCanvas {
-  private static final NativeObjRegistry<NativeCanvas> nativeObjectRegistry =
-      new NativeObjRegistry<>(NativeCanvas.class);
+/** Base class for {@link Canvas} shadow classes. Mainly contains public shadow API signatures. */
+@Implements(value = Canvas.class, shadowPicker = Picker.class)
+public abstract class ShadowCanvas {
 
-  @RealObject protected Canvas realCanvas;
-  @ReflectorObject protected CanvasReflector canvasReflector;
-
-  private final List<RoundRectPaintHistoryEvent> roundRectPaintEvents = new ArrayList<>();
-  private List<PathPaintHistoryEvent> pathPaintEvents = new ArrayList<>();
-  private List<CirclePaintHistoryEvent> circlePaintEvents = new ArrayList<>();
-  private List<ArcPaintHistoryEvent> arcPaintEvents = new ArrayList<>();
-  private List<RectPaintHistoryEvent> rectPaintEvents = new ArrayList<>();
-  private List<LinePaintHistoryEvent> linePaintEvents = new ArrayList<>();
-  private List<OvalPaintHistoryEvent> ovalPaintEvents = new ArrayList<>();
-  private List<TextHistoryEvent> drawnTextEventHistory = new ArrayList<>();
-  private Paint drawnPaint;
-  private Bitmap targetBitmap = ReflectionHelpers.callConstructor(Bitmap.class);
-  private float translateX;
-  private float translateY;
-  private float scaleX = 1;
-  private float scaleY = 1;
-  private int height;
-  private int width;
-
-  /**
-   * Returns a textual representation of the appearance of the object.
-   *
-   * @param canvas the canvas to visualize
-   * @return The textual representation of the appearance of the object.
-   */
   public static String visualize(Canvas canvas) {
-    ShadowCanvas shadowCanvas = Shadow.extract(canvas);
-    return shadowCanvas.getDescription();
-  }
-
-  @Implementation
-  protected void __constructor__(Bitmap bitmap) {
-    canvasReflector.__constructor__(bitmap);
-    this.targetBitmap = bitmap;
-  }
-
-  private long getNativeId() {
-    return RuntimeEnvironment.getApiLevel() <= KITKAT_WATCH
-        ? (int) ReflectionHelpers.getField(realCanvas, "mNativeCanvas")
-        : realCanvas.getNativeCanvasWrapper();
-  }
-
-  private NativeCanvas getNativeCanvas() {
-    return nativeObjectRegistry.getNativeObject(getNativeId());
-  }
-
-  public void appendDescription(String s) {
-    ShadowBitmap shadowBitmap = Shadow.extract(targetBitmap);
-    shadowBitmap.appendDescription(s);
-  }
-
-  public String getDescription() {
-    ShadowBitmap shadowBitmap = Shadow.extract(targetBitmap);
-    return shadowBitmap.getDescription();
-  }
-
-  @Implementation
-  protected void setBitmap(Bitmap bitmap) {
-    targetBitmap = bitmap;
-  }
-
-  @Implementation
-  protected void drawText(String text, float x, float y, Paint paint) {
-    drawnTextEventHistory.add(new TextHistoryEvent(x, y, paint, text));
-  }
-
-  @Implementation
-  protected void drawText(CharSequence text, int start, int end, float x, float y, Paint paint) {
-    drawnTextEventHistory.add(
-        new TextHistoryEvent(x, y, paint, text.subSequence(start, end).toString()));
-  }
-
-  @Implementation
-  protected void drawText(char[] text, int index, int count, float x, float y, Paint paint) {
-    drawnTextEventHistory.add(new TextHistoryEvent(x, y, paint, new String(text, index, count)));
-  }
-
-  @Implementation
-  protected void drawText(String text, int start, int end, float x, float y, Paint paint) {
-    drawnTextEventHistory.add(new TextHistoryEvent(x, y, paint, text.substring(start, end)));
-  }
-
-  @Implementation
-  protected void translate(float x, float y) {
-    this.translateX = x;
-    this.translateY = y;
-  }
-
-  @Implementation
-  protected void scale(float sx, float sy) {
-    this.scaleX = sx;
-    this.scaleY = sy;
-  }
-
-  @Implementation
-  protected void scale(float sx, float sy, float px, float py) {
-    this.scaleX = sx;
-    this.scaleY = sy;
-  }
-
-  @Implementation
-  protected void drawPaint(Paint paint) {
-    drawnPaint = paint;
-  }
-
-  @Implementation
-  protected void drawColor(int color) {
-    appendDescription("draw color " + color);
-  }
-
-  @Implementation
-  protected void drawBitmap(Bitmap bitmap, float left, float top, Paint paint) {
-    describeBitmap(bitmap, paint);
-
-    int x = (int) (left + translateX);
-    int y = (int) (top + translateY);
-    if (x != 0 || y != 0) {
-      appendDescription(" at (" + x + "," + y + ")");
-    }
-
-    if (scaleX != 1 && scaleY != 1) {
-      appendDescription(" scaled by (" + scaleX + "," + scaleY + ")");
-    }
-
-    if (bitmap != null && targetBitmap != null) {
-      ShadowBitmap shadowTargetBitmap = Shadows.shadowOf(targetBitmap);
-      shadowTargetBitmap.drawBitmap(bitmap, (int) left, (int) top);
+    if (Shadow.extract(canvas) instanceof ShadowLegacyCanvas) {
+      ShadowCanvas shadowCanvas = Shadow.extract(canvas);
+      return shadowCanvas.getDescription();
+    } else {
+      throw new UnsupportedOperationException(
+          "ShadowCanvas.visualize is only supported in legacy Canvas");
     }
   }
 
-  @Implementation
-  protected void drawBitmap(Bitmap bitmap, Rect src, Rect dst, Paint paint) {
-    describeBitmap(bitmap, paint);
+  public abstract void appendDescription(String s);
 
-    StringBuilder descriptionBuilder = new StringBuilder();
-    if (dst != null) {
-      descriptionBuilder
-          .append(" at (")
-          .append(dst.left)
-          .append(",")
-          .append(dst.top)
-          .append(") with height=")
-          .append(dst.height())
-          .append(" and width=")
-          .append(dst.width());
-    }
+  public abstract String getDescription();
 
-    if (src != null) {
-      descriptionBuilder.append(" taken from ").append(src.toString());
-    }
-    appendDescription(descriptionBuilder.toString());
-  }
+  public abstract int getPathPaintHistoryCount();
 
-  @Implementation
-  protected void drawBitmap(Bitmap bitmap, Rect src, RectF dst, Paint paint) {
-    describeBitmap(bitmap, paint);
+  public abstract int getCirclePaintHistoryCount();
 
-    StringBuilder descriptionBuilder = new StringBuilder();
-    if (dst != null) {
-      descriptionBuilder
-          .append(" at (")
-          .append(dst.left)
-          .append(",")
-          .append(dst.top)
-          .append(") with height=")
-          .append(dst.height())
-          .append(" and width=")
-          .append(dst.width());
-    }
+  public abstract int getArcPaintHistoryCount();
 
-    if (src != null) {
-      descriptionBuilder.append(" taken from ").append(src.toString());
-    }
-    appendDescription(descriptionBuilder.toString());
-  }
+  public abstract boolean hasDrawnPath();
 
-  @Implementation
-  protected void drawBitmap(Bitmap bitmap, Matrix matrix, Paint paint) {
-    describeBitmap(bitmap, paint);
+  public abstract boolean hasDrawnCircle();
 
-    ShadowMatrix shadowMatrix = Shadow.extract(matrix);
-    appendDescription(" transformed by " + shadowMatrix.getDescription());
-  }
+  public abstract Paint getDrawnPathPaint(int i);
 
-  @Implementation
-  protected void drawPath(Path path, Paint paint) {
-    pathPaintEvents.add(new PathPaintHistoryEvent(new Path(path), new Paint(paint)));
+  public abstract Path getDrawnPath(int i);
 
-    separateLines();
-    ShadowPath shadowPath = Shadow.extract(path);
-    appendDescription("Path " + shadowPath.getPoints().toString());
-  }
+  public abstract CirclePaintHistoryEvent getDrawnCircle(int i);
 
-  @Implementation
-  protected void drawCircle(float cx, float cy, float radius, Paint paint) {
-    circlePaintEvents.add(new CirclePaintHistoryEvent(cx, cy, radius, paint));
-  }
+  public abstract ArcPaintHistoryEvent getDrawnArc(int i);
 
-  @Implementation
-  protected void drawArc(
-      RectF oval, float startAngle, float sweepAngle, boolean useCenter, Paint paint) {
-    arcPaintEvents.add(new ArcPaintHistoryEvent(oval, startAngle, sweepAngle, useCenter, paint));
-  }
+  public abstract void resetCanvasHistory();
 
-  @Implementation
-  protected void drawRect(float left, float top, float right, float bottom, Paint paint) {
-    rectPaintEvents.add(new RectPaintHistoryEvent(left, top, right, bottom, paint));
+  public abstract Paint getDrawnPaint();
 
-    if (targetBitmap != null) {
-      ShadowBitmap shadowTargetBitmap = Shadows.shadowOf(targetBitmap);
-      shadowTargetBitmap.drawRect(new RectF(left, top, right, bottom), paint);
-    }
-  }
+  public abstract void setHeight(int height);
 
-  @Implementation
-  protected void drawRect(Rect r, Paint paint) {
-    rectPaintEvents.add(new RectPaintHistoryEvent(r.left, r.top, r.right, r.bottom, paint));
+  public abstract void setWidth(int width);
 
-    if (targetBitmap != null) {
-      ShadowBitmap shadowTargetBitmap = Shadows.shadowOf(targetBitmap);
-      shadowTargetBitmap.drawRect(r, paint);
-    }
-  }
+  public abstract TextHistoryEvent getDrawnTextEvent(int i);
 
-  @Implementation
-  protected void drawRoundRect(RectF rect, float rx, float ry, Paint paint) {
-    roundRectPaintEvents.add(
-        new RoundRectPaintHistoryEvent(
-            rect.left, rect.top, rect.right, rect.bottom, rx, ry, paint));
-  }
+  public abstract int getTextHistoryCount();
 
-  @Implementation
-  protected void drawLine(float startX, float startY, float stopX, float stopY, Paint paint) {
-    linePaintEvents.add(new LinePaintHistoryEvent(startX, startY, stopX, stopY, paint));
-  }
+  public abstract RectPaintHistoryEvent getDrawnRect(int i);
 
-  @Implementation
-  protected void drawOval(RectF oval, Paint paint) {
-    ovalPaintEvents.add(new OvalPaintHistoryEvent(oval, paint));
-  }
+  public abstract RectPaintHistoryEvent getLastDrawnRect();
 
-  private void describeBitmap(Bitmap bitmap, Paint paint) {
-    separateLines();
+  public abstract int getRectPaintHistoryCount();
 
-    ShadowBitmap shadowBitmap = Shadow.extract(bitmap);
-    appendDescription(shadowBitmap.getDescription());
+  public abstract RoundRectPaintHistoryEvent getDrawnRoundRect(int i);
 
-    if (paint != null) {
-      ColorFilter colorFilter = paint.getColorFilter();
-      if (colorFilter != null) {
-        appendDescription(" with " + colorFilter.getClass().getSimpleName());
-      }
-    }
-  }
+  public abstract RoundRectPaintHistoryEvent getLastDrawnRoundRect();
 
-  private void separateLines() {
-    if (getDescription().length() != 0) {
-      appendDescription("\n");
-    }
-  }
+  public abstract int getRoundRectPaintHistoryCount();
 
-  public int getPathPaintHistoryCount() {
-    return pathPaintEvents.size();
-  }
+  public abstract LinePaintHistoryEvent getDrawnLine(int i);
 
-  public int getCirclePaintHistoryCount() {
-    return circlePaintEvents.size();
-  }
+  public abstract int getLinePaintHistoryCount();
 
-  public int getArcPaintHistoryCount() {
-    return arcPaintEvents.size();
-  }
+  public abstract int getOvalPaintHistoryCount();
 
-  public boolean hasDrawnPath() {
-    return getPathPaintHistoryCount() > 0;
-  }
-
-  public boolean hasDrawnCircle() {
-    return circlePaintEvents.size() > 0;
-  }
-
-  public Paint getDrawnPathPaint(int i) {
-    return pathPaintEvents.get(i).pathPaint;
-  }
-
-  public Path getDrawnPath(int i) {
-    return pathPaintEvents.get(i).drawnPath;
-  }
-
-  public CirclePaintHistoryEvent getDrawnCircle(int i) {
-    return circlePaintEvents.get(i);
-  }
-
-  public ArcPaintHistoryEvent getDrawnArc(int i) {
-    return arcPaintEvents.get(i);
-  }
-
-  public void resetCanvasHistory() {
-    drawnTextEventHistory.clear();
-    pathPaintEvents.clear();
-    circlePaintEvents.clear();
-    rectPaintEvents.clear();
-    roundRectPaintEvents.clear();
-    linePaintEvents.clear();
-    ovalPaintEvents.clear();
-    ShadowBitmap shadowBitmap = Shadow.extract(targetBitmap);
-    shadowBitmap.setDescription("");
-  }
-
-  public Paint getDrawnPaint() {
-    return drawnPaint;
-  }
-
-  public void setHeight(int height) {
-    this.height = height;
-  }
-
-  public void setWidth(int width) {
-    this.width = width;
-  }
-
-  @Implementation
-  protected int getWidth() {
-    if (width == 0) {
-      return targetBitmap.getWidth();
-    }
-    return width;
-  }
-
-  @Implementation
-  protected int getHeight() {
-    if (height == 0) {
-      return targetBitmap.getHeight();
-    }
-    return height;
-  }
-
-  @Implementation
-  protected boolean getClipBounds(Rect bounds) {
-    Preconditions.checkNotNull(bounds);
-    if (targetBitmap == null) {
-      return false;
-    }
-    bounds.set(0, 0, targetBitmap.getWidth(), targetBitmap.getHeight());
-    return !bounds.isEmpty();
-  }
-
-  public TextHistoryEvent getDrawnTextEvent(int i) {
-    return drawnTextEventHistory.get(i);
-  }
-
-  public int getTextHistoryCount() {
-    return drawnTextEventHistory.size();
-  }
-
-  public RectPaintHistoryEvent getDrawnRect(int i) {
-    return rectPaintEvents.get(i);
-  }
-
-  public RectPaintHistoryEvent getLastDrawnRect() {
-    return rectPaintEvents.get(rectPaintEvents.size() - 1);
-  }
-
-  public int getRectPaintHistoryCount() {
-    return rectPaintEvents.size();
-  }
-
-  public RoundRectPaintHistoryEvent getDrawnRoundRect(int i) {
-    return roundRectPaintEvents.get(i);
-  }
-
-  public RoundRectPaintHistoryEvent getLastDrawnRoundRect() {
-    return roundRectPaintEvents.get(roundRectPaintEvents.size() - 1);
-  }
-
-  public int getRoundRectPaintHistoryCount() {
-    return roundRectPaintEvents.size();
-  }
-
-  public LinePaintHistoryEvent getDrawnLine(int i) {
-    return linePaintEvents.get(i);
-  }
-
-  public int getLinePaintHistoryCount() {
-    return linePaintEvents.size();
-  }
-
-  public int getOvalPaintHistoryCount() {
-    return ovalPaintEvents.size();
-  }
-
-  public OvalPaintHistoryEvent getDrawnOval(int i) {
-    return ovalPaintEvents.get(i);
-  }
-
-  @Implementation(maxSdk = N_MR1)
-  protected int save() {
-    return getNativeCanvas().save();
-  }
-
-  @Implementation(maxSdk = N_MR1)
-  protected void restore() {
-    getNativeCanvas().restore();
-  }
-
-  @Implementation(maxSdk = N_MR1)
-  protected int getSaveCount() {
-    return getNativeCanvas().getSaveCount();
-  }
-
-  @Implementation(maxSdk = N_MR1)
-  protected void restoreToCount(int saveCount) {
-    getNativeCanvas().restoreToCount(saveCount);
-  }
-
-  @Implementation(minSdk = KITKAT)
-  protected void release() {
-    nativeObjectRegistry.unregister(getNativeId());
-    canvasReflector.release();
-  }
-
-  @Implementation(maxSdk = KITKAT_WATCH)
-  protected static int initRaster(int bitmapHandle) {
-    return (int) nativeObjectRegistry.register(new NativeCanvas());
-  }
-
-  @Implementation(minSdk = LOLLIPOP, maxSdk = LOLLIPOP_MR1)
-  protected static long initRaster(long bitmapHandle) {
-    return nativeObjectRegistry.register(new NativeCanvas());
-  }
-
-  @Implementation(minSdk = M, maxSdk = N_MR1)
-  protected static long initRaster(Bitmap bitmap) {
-    return nativeObjectRegistry.register(new NativeCanvas());
-  }
-
-  @Implementation(minSdk = O, maxSdk = P)
-  protected static long nInitRaster(Bitmap bitmap) {
-    return nativeObjectRegistry.register(new NativeCanvas());
-  }
-
-  @Implementation(minSdk = Q)
-  protected static long nInitRaster(long bitmapHandle) {
-    return nativeObjectRegistry.register(new NativeCanvas());
-  }
-
-  @Implementation(minSdk = O)
-  protected static int nGetSaveCount(long canvasHandle) {
-    return nativeObjectRegistry.getNativeObject(canvasHandle).getSaveCount();
-  }
-
-  @Implementation(minSdk = O)
-  protected static int nSave(long canvasHandle, int saveFlags) {
-    return nativeObjectRegistry.getNativeObject(canvasHandle).save();
-  }
-
-  @Implementation(maxSdk = KITKAT_WATCH)
-  protected static int native_saveLayer(int nativeCanvas, RectF bounds, int paint, int layerFlags) {
-    return nativeObjectRegistry.getNativeObject(nativeCanvas).save();
-  }
-
-  @Implementation(maxSdk = KITKAT_WATCH)
-  protected static int native_saveLayer(
-      int nativeCanvas, float l, float t, float r, float b, int paint, int layerFlags) {
-    return nativeObjectRegistry.getNativeObject(nativeCanvas).save();
-  }
-
-  @Implementation(minSdk = LOLLIPOP, maxSdk = N_MR1)
-  protected static int native_saveLayer(
-      long nativeCanvas, float l, float t, float r, float b, long nativePaint, int layerFlags) {
-    return nativeObjectRegistry.getNativeObject(nativeCanvas).save();
-  }
-
-  @Implementation(minSdk = O, maxSdk = R)
-  protected static int nSaveLayer(
-      long nativeCanvas, float l, float t, float r, float b, long nativePaint, int layerFlags) {
-    return nativeObjectRegistry.getNativeObject(nativeCanvas).save();
-  }
-
-  @Implementation(minSdk = S)
-  protected static int nSaveLayer(
-      long nativeCanvas, float l, float t, float r, float b, long nativePaint) {
-    return nativeObjectRegistry.getNativeObject(nativeCanvas).save();
-  }
-
-  @Implementation(maxSdk = KITKAT_WATCH)
-  protected static int native_saveLayerAlpha(
-      int nativeCanvas, RectF bounds, int alpha, int layerFlags) {
-    return nativeObjectRegistry.getNativeObject(nativeCanvas).save();
-  }
-
-  @Implementation(maxSdk = KITKAT_WATCH)
-  protected static int native_saveLayerAlpha(
-      int nativeCanvas, float l, float t, float r, float b, int alpha, int layerFlags) {
-    return nativeObjectRegistry.getNativeObject(nativeCanvas).save();
-  }
-
-  @Implementation(minSdk = LOLLIPOP, maxSdk = N_MR1)
-  protected static int native_saveLayerAlpha(
-      long nativeCanvas, float l, float t, float r, float b, int alpha, int layerFlags) {
-    return nativeObjectRegistry.getNativeObject(nativeCanvas).save();
-  }
-
-  @Implementation(minSdk = O, maxSdk = R)
-  protected static int nSaveLayerAlpha(
-      long nativeCanvas, float l, float t, float r, float b, int alpha, int layerFlags) {
-    return nativeObjectRegistry.getNativeObject(nativeCanvas).save();
-  }
-
-  @Implementation(minSdk = S)
-  protected static int nSaveLayerAlpha(
-      long nativeCanvas, float l, float t, float r, float b, int alpha) {
-    return nativeObjectRegistry.getNativeObject(nativeCanvas).save();
-  }
-
-  @Implementation(minSdk = O)
-  protected static boolean nRestore(long canvasHandle) {
-    return nativeObjectRegistry.getNativeObject(canvasHandle).restore();
-  }
-
-  @Implementation(minSdk = O)
-  protected static void nRestoreToCount(long canvasHandle, int saveCount) {
-    nativeObjectRegistry.getNativeObject(canvasHandle).restoreToCount(saveCount);
-  }
-
-  @Resetter
-  public static void reset() {
-    nativeObjectRegistry.clear();
-  }
+  public abstract OvalPaintHistoryEvent getDrawnOval(int i);
 
   public static class LinePaintHistoryEvent {
     public Paint paint;
@@ -585,8 +83,7 @@
     public float stopX;
     public float stopY;
 
-    private LinePaintHistoryEvent(
-        float startX, float startY, float stopX, float stopY, Paint paint) {
+    LinePaintHistoryEvent(float startX, float startY, float stopX, float stopY, Paint paint) {
       this.paint = new Paint(paint);
       this.paint.setColor(paint.getColor());
       this.paint.setStrokeWidth(paint.getStrokeWidth());
@@ -601,7 +98,7 @@
     public final RectF oval;
     public final Paint paint;
 
-    private OvalPaintHistoryEvent(RectF oval, Paint paint) {
+    OvalPaintHistoryEvent(RectF oval, Paint paint) {
       this.oval = new RectF(oval);
       this.paint = new Paint(paint);
       this.paint.setColor(paint.getColor());
@@ -617,7 +114,7 @@
     public final float right;
     public final float bottom;
 
-    private RectPaintHistoryEvent(float left, float top, float right, float bottom, Paint paint) {
+    RectPaintHistoryEvent(float left, float top, float right, float bottom, Paint paint) {
       this.rect = new RectF(left, top, right, bottom);
       this.paint = new Paint(paint);
       this.paint.setColor(paint.getColor());
@@ -642,7 +139,7 @@
     public final float rx;
     public final float ry;
 
-    private RoundRectPaintHistoryEvent(
+    RoundRectPaintHistoryEvent(
         float left, float top, float right, float bottom, float rx, float ry, Paint paint) {
       this.rect = new RectF(left, top, right, bottom);
       this.paint = new Paint(paint);
@@ -659,23 +156,13 @@
     }
   }
 
-  private static class PathPaintHistoryEvent {
-    private final Path drawnPath;
-    private final Paint pathPaint;
-
-    PathPaintHistoryEvent(Path drawnPath, Paint pathPaint) {
-      this.drawnPath = drawnPath;
-      this.pathPaint = pathPaint;
-    }
-  }
-
   public static class CirclePaintHistoryEvent {
     public final float centerX;
     public final float centerY;
     public final float radius;
     public final Paint paint;
 
-    private CirclePaintHistoryEvent(float centerX, float centerY, float radius, Paint paint) {
+    CirclePaintHistoryEvent(float centerX, float centerY, float radius, Paint paint) {
       this.centerX = centerX;
       this.centerY = centerY;
       this.radius = radius;
@@ -706,7 +193,7 @@
     public final Paint paint;
     public final String text;
 
-    private TextHistoryEvent(float x, float y, Paint paint, String text) {
+    TextHistoryEvent(float x, float y, Paint paint, String text) {
       this.x = x;
       this.y = y;
       this.paint = paint;
@@ -714,40 +201,10 @@
     }
   }
 
-  @SuppressWarnings("MemberName")
-  @ForType(Canvas.class)
-  private interface CanvasReflector {
-    @Direct
-    void __constructor__(Bitmap bitmap);
-
-    @Direct
-    void release();
-  }
-
-  private static class NativeCanvas {
-    private int saveCount = 1;
-
-    int save() {
-      return saveCount++;
-    }
-
-    boolean restore() {
-      if (saveCount > 1) {
-        saveCount--;
-        return true;
-      } else {
-        return false;
-      }
-    }
-
-    int getSaveCount() {
-      return saveCount;
-    }
-
-    void restoreToCount(int saveCount) {
-      if (saveCount > 0) {
-        this.saveCount = saveCount;
-      }
+  /** Shadow picker for {@link Canvas}. */
+  public static final class Picker extends GraphicsShadowPicker<Object> {
+    public Picker() {
+      super(ShadowLegacyCanvas.class, ShadowNativeCanvas.class);
     }
   }
 }
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowCarrierConfigManager.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowCarrierConfigManager.java
index 8a17212..39b9428 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowCarrierConfigManager.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowCarrierConfigManager.java
@@ -16,6 +16,7 @@
 
   private final HashMap<Integer, PersistableBundle> bundles = new HashMap<>();
   private final HashMap<Integer, PersistableBundle> overrideBundles = new HashMap<>();
+  private boolean readPhoneStatePermission = true;
 
   /**
    * Returns {@link android.os.PersistableBundle} previously set by {@link #overrideConfig} or
@@ -24,6 +25,7 @@
    */
   @Implementation
   public PersistableBundle getConfigForSubId(int subId) {
+    checkReadPhoneStatePermission();
     if (overrideBundles.containsKey(subId) && overrideBundles.get(subId) != null) {
       return overrideBundles.get(subId);
     }
@@ -33,6 +35,10 @@
     return new PersistableBundle();
   }
 
+  public void setReadPhoneStatePermission(boolean readPhoneStatePermission) {
+    this.readPhoneStatePermission = readPhoneStatePermission;
+  }
+
   /**
    * Sets that the {@code config} PersistableBundle for a particular {@code subId}; controls the
    * return value of {@link CarrierConfigManager#getConfigForSubId()}.
@@ -52,4 +58,10 @@
   protected void overrideConfig(int subId, @Nullable PersistableBundle config) {
     overrideBundles.put(subId, config);
   }
+
+  private void checkReadPhoneStatePermission() {
+    if (!readPhoneStatePermission) {
+      throw new SecurityException();
+    }
+  }
 }
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowContentProvider.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowContentProvider.java
index 59e275e..a824e90 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowContentProvider.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowContentProvider.java
@@ -1,6 +1,7 @@
 package org.robolectric.shadows;
 
 import static android.os.Build.VERSION_CODES.KITKAT;
+import static android.os.Build.VERSION_CODES.Q;
 import static org.robolectric.util.reflector.Reflector.reflector;
 
 import android.content.ContentProvider;
@@ -10,14 +11,17 @@
 import org.robolectric.util.reflector.Direct;
 import org.robolectric.util.reflector.ForType;
 
-@Implements(ContentProvider.class)
+/** Shadow for {@link ContentProvider}. */
+@Implements(value = ContentProvider.class, looseSignatures = true)
 public class ShadowContentProvider {
   @RealObject private ContentProvider realContentProvider;
 
   private String callingPackage;
 
-  public void setCallingPackage(String callingPackage) {
-    this.callingPackage = callingPackage;
+  @Implementation(minSdk = Q, maxSdk = Q)
+  public Object setCallingPackage(Object callingPackage) {
+    this.callingPackage = (String) callingPackage;
+    return callingPackage;
   }
 
   @Implementation(minSdk = KITKAT)
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowContentResolver.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowContentResolver.java
index 98adb47..0b86844 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowContentResolver.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowContentResolver.java
@@ -73,20 +73,20 @@
   @RealObject ContentResolver realContentResolver;
 
   private BaseCursor cursor;
-  private final List<Statement> statements = new CopyOnWriteArrayList<>();
-  private final List<InsertStatement> insertStatements = new CopyOnWriteArrayList<>();
-  private final List<UpdateStatement> updateStatements = new CopyOnWriteArrayList<>();
-  private final List<DeleteStatement> deleteStatements = new CopyOnWriteArrayList<>();
-  private List<NotifiedUri> notifiedUris = new ArrayList<>();
-  private Map<Uri, BaseCursor> uriCursorMap = new HashMap<>();
-  private Map<Uri, Supplier<InputStream>> inputStreamMap = new HashMap<>();
-  private Map<Uri, Supplier<OutputStream>> outputStreamMap = new HashMap<>();
-  private final Map<String, List<ContentProviderOperation>> contentProviderOperations =
+  private static final List<Statement> statements = new CopyOnWriteArrayList<>();
+  private static final List<InsertStatement> insertStatements = new CopyOnWriteArrayList<>();
+  private static final List<UpdateStatement> updateStatements = new CopyOnWriteArrayList<>();
+  private static final List<DeleteStatement> deleteStatements = new CopyOnWriteArrayList<>();
+  private static final List<NotifiedUri> notifiedUris = new ArrayList<>();
+  private static final Map<Uri, BaseCursor> uriCursorMap = new HashMap<>();
+  private static final Map<Uri, Supplier<InputStream>> inputStreamMap = new HashMap<>();
+  private static final Map<Uri, Supplier<OutputStream>> outputStreamMap = new HashMap<>();
+  private static final Map<String, List<ContentProviderOperation>> contentProviderOperations =
       new HashMap<>();
-  private ContentProviderResult[] contentProviderResults;
-  private final List<UriPermission> uriPermissions = new ArrayList<>();
+  private static ContentProviderResult[] contentProviderResults;
+  private static final List<UriPermission> uriPermissions = new ArrayList<>();
 
-  private final CopyOnWriteArrayList<ContentObserverEntry> contentObservers =
+  private static final CopyOnWriteArrayList<ContentObserverEntry> contentObservers =
       new CopyOnWriteArrayList<>();
 
   private static final Map<String, Map<Account, Status>> syncableAccounts = new HashMap<>();
@@ -98,6 +98,18 @@
 
   @Resetter
   public static void reset() {
+    statements.clear();
+    insertStatements.clear();
+    updateStatements.clear();
+    deleteStatements.clear();
+    notifiedUris.clear();
+    uriCursorMap.clear();
+    inputStreamMap.clear();
+    outputStreamMap.clear();
+    contentProviderOperations.clear();
+    contentProviderResults = null;
+    uriPermissions.clear();
+    contentObservers.clear();
     syncableAccounts.clear();
     providers.clear();
     masterSyncAutomatically = false;
@@ -788,7 +800,7 @@
    */
   @Deprecated
   public void setCursor(Uri uri, BaseCursor cursorForUri) {
-    this.uriCursorMap.put(uri, cursorForUri);
+    uriCursorMap.put(uri, cursorForUri);
   }
 
   /**
@@ -883,7 +895,7 @@
 
   @Deprecated
   public void setContentProviderResult(ContentProviderResult[] contentProviderResults) {
-    this.contentProviderResults = contentProviderResults;
+    ShadowContentResolver.contentProviderResults = contentProviderResults;
   }
 
   private final Map<Uri, RuntimeException> registerContentProviderExceptions = new HashMap<>();
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowDevicePolicyManager.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowDevicePolicyManager.java
index c768882..dbaa353 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowDevicePolicyManager.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowDevicePolicyManager.java
@@ -1,5 +1,8 @@
 package org.robolectric.shadows;
 
+import static android.app.admin.DevicePolicyManager.LOCK_TASK_FEATURE_HOME;
+import static android.app.admin.DevicePolicyManager.LOCK_TASK_FEATURE_NOTIFICATIONS;
+import static android.app.admin.DevicePolicyManager.LOCK_TASK_FEATURE_OVERVIEW;
 import static android.os.Build.VERSION_CODES.JELLY_BEAN_MR1;
 import static android.os.Build.VERSION_CODES.JELLY_BEAN_MR2;
 import static android.os.Build.VERSION_CODES.LOLLIPOP;
@@ -128,6 +131,7 @@
   private final Map<ComponentName, CharSequence> longSupportMessageMap = new HashMap<>();
   private final Set<ComponentName> componentsWithActivatedTokens = new HashSet<>();
   private Collection<String> packagesToFailForSetApplicationHidden = Collections.emptySet();
+  private int lockTaskFeatures;
   private final List<String> lockTaskPackages = new ArrayList<>();
   private Context context;
   private ApplicationPackageManager applicationPackageManager;
@@ -1236,6 +1240,31 @@
     return policyGrantedSet != null && policyGrantedSet.contains(usesPolicy);
   }
 
+  @Implementation(minSdk = P)
+  protected int getLockTaskFeatures(ComponentName admin) {
+    Objects.requireNonNull(admin, "ComponentName is null");
+    enforceDeviceOwnerOrProfileOwner(admin);
+    return lockTaskFeatures;
+  }
+
+  @Implementation(minSdk = P)
+  protected void setLockTaskFeatures(ComponentName admin, int flags) {
+    Objects.requireNonNull(admin, "ComponentName is null");
+    enforceDeviceOwnerOrProfileOwner(admin);
+    // Throw if Overview is used without Home.
+    boolean hasHome = (flags & LOCK_TASK_FEATURE_HOME) != 0;
+    boolean hasOverview = (flags & LOCK_TASK_FEATURE_OVERVIEW) != 0;
+    Preconditions.checkArgument(
+        hasHome || !hasOverview,
+        "Cannot use LOCK_TASK_FEATURE_OVERVIEW without LOCK_TASK_FEATURE_HOME");
+    boolean hasNotification = (flags & LOCK_TASK_FEATURE_NOTIFICATIONS) != 0;
+    Preconditions.checkArgument(
+        hasHome || !hasNotification,
+        "Cannot use LOCK_TASK_FEATURE_NOTIFICATIONS without LOCK_TASK_FEATURE_HOME");
+
+    lockTaskFeatures = flags;
+  }
+
   @Implementation(minSdk = LOLLIPOP)
   protected void setLockTaskPackages(@NonNull ComponentName admin, String[] packages) {
     enforceDeviceOwnerOrProfileOwner(admin);
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowDisplayListCanvas.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowDisplayListCanvas.java
index 71b2ca1..02d2143 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowDisplayListCanvas.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowDisplayListCanvas.java
@@ -16,7 +16,7 @@
     isInAndroidSdk = false,
     minSdk = M,
     maxSdk = R)
-public class ShadowDisplayListCanvas extends ShadowCanvas {
+public class ShadowDisplayListCanvas extends ShadowLegacyCanvas {
 
   @Implementation(minSdk = O, maxSdk = P)
   protected static long nCreateDisplayListCanvas(long node, int width, int height) {
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowImsMmTelManager.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowImsMmTelManager.java
index 59dd707..1675a02 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowImsMmTelManager.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowImsMmTelManager.java
@@ -11,8 +11,9 @@
 import android.telephony.ims.ImsException;
 import android.telephony.ims.ImsMmTelManager;
 import android.telephony.ims.ImsMmTelManager.CapabilityCallback;
-import android.telephony.ims.ImsMmTelManager.RegistrationCallback;
 import android.telephony.ims.ImsReasonInfo;
+import android.telephony.ims.ImsRegistrationAttributes;
+import android.telephony.ims.RegistrationManager;
 import android.telephony.ims.feature.MmTelFeature.MmTelCapabilities;
 import android.telephony.ims.stub.ImsRegistrationImplBase;
 import android.util.ArrayMap;
@@ -43,8 +44,10 @@
 
   protected static final Map<Integer, ImsMmTelManager> existingInstances = new ArrayMap<>();
 
-  private final Map<RegistrationCallback, Executor> registrationCallbackExecutorMap =
-      new ArrayMap<>();
+  private final Map<ImsMmTelManager.RegistrationCallback, Executor>
+      registrationCallbackExecutorMap = new ArrayMap<>();
+  private final Map<RegistrationManager.RegistrationCallback, Executor>
+      registrationManagerCallbackExecutorMap = new ArrayMap<>();
   private final Map<CapabilityCallback, Executor> capabilityCallbackExecutorMap = new ArrayMap<>();
   private boolean imsAvailableOnDevice = true;
   private MmTelCapabilities mmTelCapabilitiesAvailable =
@@ -70,7 +73,7 @@
   @RequiresPermission(Manifest.permission.READ_PRIVILEGED_PHONE_STATE)
   @Implementation
   protected void registerImsRegistrationCallback(
-      @NonNull @CallbackExecutor Executor executor, @NonNull RegistrationCallback c)
+      @NonNull @CallbackExecutor Executor executor, @NonNull ImsMmTelManager.RegistrationCallback c)
       throws ImsException {
     if (!imsAvailableOnDevice) {
       throw new ImsException(
@@ -79,12 +82,41 @@
     registrationCallbackExecutorMap.put(c, executor);
   }
 
+  @RequiresPermission(
+      anyOf = {
+        android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE,
+        android.Manifest.permission.READ_PRECISE_PHONE_STATE
+      })
+  @Implementation(minSdk = VERSION_CODES.R)
+  protected void registerImsRegistrationCallback(
+      @NonNull @CallbackExecutor Executor executor,
+      @NonNull RegistrationManager.RegistrationCallback c)
+      throws ImsException {
+    if (!imsAvailableOnDevice) {
+      throw new ImsException(
+          "IMS not available on device.", ImsException.CODE_ERROR_UNSUPPORTED_OPERATION);
+    }
+    registrationManagerCallbackExecutorMap.put(c, executor);
+  }
+
   @RequiresPermission(Manifest.permission.READ_PRIVILEGED_PHONE_STATE)
   @Implementation
-  protected void unregisterImsRegistrationCallback(@NonNull RegistrationCallback c) {
+  protected void unregisterImsRegistrationCallback(
+      @NonNull ImsMmTelManager.RegistrationCallback c) {
     registrationCallbackExecutorMap.remove(c);
   }
 
+  @RequiresPermission(
+      anyOf = {
+        android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE,
+        android.Manifest.permission.READ_PRECISE_PHONE_STATE
+      })
+  @Implementation(minSdk = VERSION_CODES.R)
+  protected void unregisterImsRegistrationCallback(
+      @NonNull RegistrationManager.RegistrationCallback c) {
+    registrationManagerCallbackExecutorMap.remove(c);
+  }
+
   /**
    * Triggers {@link RegistrationCallback#onRegistering(int)} for all registered {@link
    * RegistrationCallback} callbacks.
@@ -92,10 +124,23 @@
    * @see #registerImsRegistrationCallback(Executor, RegistrationCallback)
    */
   public void setImsRegistering(int imsRegistrationTech) {
-    for (Map.Entry<RegistrationCallback, Executor> entry :
+    for (Map.Entry<ImsMmTelManager.RegistrationCallback, Executor> entry :
         registrationCallbackExecutorMap.entrySet()) {
       entry.getValue().execute(() -> entry.getKey().onRegistering(imsRegistrationTech));
     }
+
+    for (Map.Entry<RegistrationManager.RegistrationCallback, Executor> entry :
+        registrationManagerCallbackExecutorMap.entrySet()) {
+      entry.getValue().execute(() -> entry.getKey().onRegistering(imsRegistrationTech));
+    }
+  }
+
+  @RequiresApi(api = VERSION_CODES.S)
+  public void setImsRegistering(@NonNull ImsRegistrationAttributes attrs) {
+    for (Map.Entry<RegistrationManager.RegistrationCallback, Executor> entry :
+        registrationManagerCallbackExecutorMap.entrySet()) {
+      entry.getValue().execute(() -> entry.getKey().onRegistering(attrs));
+    }
   }
 
   /**
@@ -106,10 +151,23 @@
    */
   public void setImsRegistered(int imsRegistrationTech) {
     this.imsRegistrationTech = imsRegistrationTech;
-    for (Map.Entry<RegistrationCallback, Executor> entry :
+    for (Map.Entry<ImsMmTelManager.RegistrationCallback, Executor> entry :
         registrationCallbackExecutorMap.entrySet()) {
       entry.getValue().execute(() -> entry.getKey().onRegistered(imsRegistrationTech));
     }
+
+    for (Map.Entry<RegistrationManager.RegistrationCallback, Executor> entry :
+        registrationManagerCallbackExecutorMap.entrySet()) {
+      entry.getValue().execute(() -> entry.getKey().onRegistered(imsRegistrationTech));
+    }
+  }
+
+  @RequiresApi(api = VERSION_CODES.S)
+  public void setImsRegistered(@NonNull ImsRegistrationAttributes attrs) {
+    for (Map.Entry<RegistrationManager.RegistrationCallback, Executor> entry :
+        registrationManagerCallbackExecutorMap.entrySet()) {
+      entry.getValue().execute(() -> entry.getKey().onRegistered(attrs));
+    }
   }
 
   /**
@@ -120,10 +178,30 @@
    */
   public void setImsUnregistered(@NonNull ImsReasonInfo imsReasonInfo) {
     this.imsRegistrationTech = ImsRegistrationImplBase.REGISTRATION_TECH_NONE;
-    for (Map.Entry<RegistrationCallback, Executor> entry :
+    for (Map.Entry<ImsMmTelManager.RegistrationCallback, Executor> entry :
         registrationCallbackExecutorMap.entrySet()) {
       entry.getValue().execute(() -> entry.getKey().onUnregistered(imsReasonInfo));
     }
+
+    for (Map.Entry<RegistrationManager.RegistrationCallback, Executor> entry :
+        registrationManagerCallbackExecutorMap.entrySet()) {
+      entry.getValue().execute(() -> entry.getKey().onUnregistered(imsReasonInfo));
+    }
+  }
+
+  /**
+   * Triggers {@link RegistrationCallback#onTechnologyChangeFailed(int, ImsReasonInfo)} for all
+   * registered {@link RegistrationCallback} callbacks.
+   *
+   * @see #registerImsRegistrationCallback(Executor, RegistrationCallback)
+   */
+  public void setOnTechnologyChangeFailed(int imsRadioTech, @NonNull ImsReasonInfo imsReasonInfo) {
+    for (Map.Entry<RegistrationManager.RegistrationCallback, Executor> entry :
+        registrationManagerCallbackExecutorMap.entrySet()) {
+      entry
+          .getValue()
+          .execute(() -> entry.getKey().onTechnologyChangeFailed(imsRadioTech, imsReasonInfo));
+    }
   }
 
   @RequiresPermission(Manifest.permission.READ_PRIVILEGED_PHONE_STATE)
@@ -174,7 +252,6 @@
   }
 
   /** Returns only one instance per subscription id. */
-  @RequiresApi(api = VERSION_CODES.Q)
   @Implementation
   protected static ImsMmTelManager createForSubscriptionId(int subId) {
     if (!SubscriptionManager.isValidSubscriptionId(subId)) {
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowInsetsController.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowInsetsController.java
new file mode 100644
index 0000000..1c47aab
--- /dev/null
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowInsetsController.java
@@ -0,0 +1,77 @@
+package org.robolectric.shadows;
+
+import android.os.Build;
+import android.view.InsetsController;
+import android.view.WindowInsets;
+import androidx.annotation.RequiresApi;
+import org.robolectric.annotation.Implementation;
+import org.robolectric.annotation.Implements;
+import org.robolectric.annotation.ReflectorObject;
+import org.robolectric.util.reflector.Direct;
+import org.robolectric.util.reflector.ForType;
+
+/** Intercepts calls to [InsetsController] to monitor system bars functionality (hide/show). */
+@Implements(value = InsetsController.class, minSdk = Build.VERSION_CODES.R, isInAndroidSdk = false)
+@RequiresApi(Build.VERSION_CODES.R)
+public class ShadowInsetsController {
+  @ReflectorObject private InsetsControllerReflector insetsControllerReflector;
+
+  /**
+   * Intercepts calls to [InsetsController.show] to detect requested changes to the system
+   * status/nav bar visibility.
+   */
+  @Implementation
+  protected void show(int types) {
+    if (hasStatusBarType(types)) {
+      ShadowViewRootImpl.setIsStatusBarVisible(true);
+    }
+
+    if (hasNavigationBarType(types)) {
+      ShadowViewRootImpl.setIsNavigationBarVisible(true);
+    }
+
+    insetsControllerReflector.show(types);
+  }
+
+  /**
+   * Intercepts calls to [InsetsController.hide] to detect requested changes to the system
+   * status/nav bar visibility.
+   */
+  @Implementation
+  public void hide(int types) {
+    if (hasStatusBarType(types)) {
+      ShadowViewRootImpl.setIsStatusBarVisible(false);
+    }
+
+    if (hasNavigationBarType(types)) {
+      ShadowViewRootImpl.setIsNavigationBarVisible(false);
+    }
+
+    insetsControllerReflector.hide(types);
+  }
+
+  /** Returns true if the given flags contain the mask for the system status bar. */
+  private boolean hasStatusBarType(int types) {
+    return hasTypeMask(types, WindowInsets.Type.statusBars());
+  }
+
+  /** Returns true if the given flags contain the mask for the system navigation bar. */
+  private boolean hasNavigationBarType(int types) {
+    return hasTypeMask(types, WindowInsets.Type.navigationBars());
+  }
+
+  /** Returns true if the given flags contains the requested type mask. */
+  private boolean hasTypeMask(int types, int typeMask) {
+    return (types & typeMask) == typeMask;
+  }
+
+  /** Reflector for [InsetsController] to use for direct (non-intercepted) calls. */
+  @ForType(InsetsController.class)
+  interface InsetsControllerReflector {
+    @Direct
+    void show(int types);
+
+    @Direct
+    void hide(int types);
+  }
+}
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowLayoutAnimationController.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowLayoutAnimationController.java
deleted file mode 100644
index ec66505..0000000
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowLayoutAnimationController.java
+++ /dev/null
@@ -1,24 +0,0 @@
-package org.robolectric.shadows;
-
-import android.view.animation.LayoutAnimationController;
-import org.robolectric.annotation.Implements;
-import org.robolectric.annotation.RealObject;
-
-@Implements(LayoutAnimationController.class)
-public class ShadowLayoutAnimationController {
-  @RealObject
-  private LayoutAnimationController realAnimation;
-
-  private int loadedFromResourceId = -1;
-
-  public void setLoadedFromResourceId(int loadedFromResourceId) {
-    this.loadedFromResourceId = loadedFromResourceId;
-  }
-
-  public int getLoadedFromResourceId() {
-    if (loadedFromResourceId == -1) {
-      throw new IllegalStateException("not loaded from a resource");
-    }
-    return loadedFromResourceId;
-  }
-}
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowLegacyBitmap.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowLegacyBitmap.java
new file mode 100644
index 0000000..79b1cbc
--- /dev/null
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowLegacyBitmap.java
@@ -0,0 +1,922 @@
+package org.robolectric.shadows;
+
+import static android.os.Build.VERSION_CODES.JELLY_BEAN_MR1;
+import static android.os.Build.VERSION_CODES.KITKAT;
+import static android.os.Build.VERSION_CODES.M;
+import static android.os.Build.VERSION_CODES.O;
+import static android.os.Build.VERSION_CODES.Q;
+import static android.os.Build.VERSION_CODES.S;
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkNotNull;
+import static java.lang.Integer.max;
+import static java.lang.Integer.min;
+
+import android.graphics.Bitmap;
+import android.graphics.ColorSpace;
+import android.graphics.Matrix;
+import android.graphics.Paint;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.os.Build;
+import android.os.Parcel;
+import android.util.DisplayMetrics;
+import java.awt.Color;
+import java.awt.Graphics2D;
+import java.awt.geom.Rectangle2D;
+import java.awt.image.BufferedImage;
+import java.awt.image.ColorModel;
+import java.awt.image.DataBufferInt;
+import java.awt.image.WritableRaster;
+import java.io.FileDescriptor;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.nio.Buffer;
+import java.nio.ByteBuffer;
+import java.nio.IntBuffer;
+import java.util.Arrays;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.annotation.Implementation;
+import org.robolectric.annotation.Implements;
+import org.robolectric.annotation.RealObject;
+import org.robolectric.shadow.api.Shadow;
+import org.robolectric.util.ReflectionHelpers;
+
+@SuppressWarnings({"UnusedDeclaration"})
+@Implements(value = Bitmap.class, isInAndroidSdk = false)
+public class ShadowLegacyBitmap extends ShadowBitmap {
+  /** Number of bytes used internally to represent each pixel */
+  private static final int INTERNAL_BYTES_PER_PIXEL = 4;
+
+  int createdFromResId = -1;
+  String createdFromPath;
+  InputStream createdFromStream;
+  FileDescriptor createdFromFileDescriptor;
+  byte[] createdFromBytes;
+  @RealObject private Bitmap realBitmap;
+  private Bitmap createdFromBitmap;
+  private Bitmap scaledFromBitmap;
+  private int createdFromX = -1;
+  private int createdFromY = -1;
+  private int createdFromWidth = -1;
+  private int createdFromHeight = -1;
+  private int[] createdFromColors;
+  private Matrix createdFromMatrix;
+  private boolean createdFromFilter;
+
+  private int width;
+  private int height;
+  private BufferedImage bufferedImage;
+  private Bitmap.Config config;
+  private boolean mutable = true;
+  private String description = "";
+  private boolean recycled = false;
+  private boolean hasMipMap;
+  private boolean requestPremultiplied = true;
+  private boolean hasAlpha;
+  private ColorSpace colorSpace;
+
+  @Implementation
+  protected static Bitmap createBitmap(int width, int height, Bitmap.Config config) {
+    return createBitmap((DisplayMetrics) null, width, height, config);
+  }
+
+  @Implementation(minSdk = JELLY_BEAN_MR1)
+  protected static Bitmap createBitmap(
+      DisplayMetrics displayMetrics, int width, int height, Bitmap.Config config) {
+    return createBitmap(displayMetrics, width, height, config, true);
+  }
+
+  @Implementation(minSdk = JELLY_BEAN_MR1)
+  protected static Bitmap createBitmap(
+      DisplayMetrics displayMetrics,
+      int width,
+      int height,
+      Bitmap.Config config,
+      boolean hasAlpha) {
+    if (width <= 0 || height <= 0) {
+      throw new IllegalArgumentException("width and height must be > 0");
+    }
+    checkNotNull(config);
+    Bitmap scaledBitmap = ReflectionHelpers.callConstructor(Bitmap.class);
+    ShadowLegacyBitmap shadowBitmap = Shadow.extract(scaledBitmap);
+    shadowBitmap.setDescription("Bitmap (" + width + " x " + height + ")");
+
+    shadowBitmap.width = width;
+    shadowBitmap.height = height;
+    shadowBitmap.config = config;
+    shadowBitmap.hasAlpha = hasAlpha;
+    shadowBitmap.setMutable(true);
+    if (displayMetrics != null) {
+      scaledBitmap.setDensity(displayMetrics.densityDpi);
+    }
+    shadowBitmap.bufferedImage = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
+    if (RuntimeEnvironment.getApiLevel() >= O) {
+      shadowBitmap.colorSpace = ColorSpace.get(ColorSpace.Named.SRGB);
+    }
+    return scaledBitmap;
+  }
+
+  @Implementation(minSdk = O)
+  protected static Bitmap createBitmap(
+      int width, int height, Bitmap.Config config, boolean hasAlpha, ColorSpace colorSpace) {
+    checkArgument(colorSpace != null || config == Bitmap.Config.ALPHA_8);
+    Bitmap bitmap = createBitmap(null, width, height, config, hasAlpha);
+    ShadowLegacyBitmap shadowBitmap = Shadow.extract(bitmap);
+    shadowBitmap.colorSpace = colorSpace;
+    return bitmap;
+  }
+
+  @Implementation
+  protected static Bitmap createBitmap(
+      Bitmap src, int x, int y, int width, int height, Matrix matrix, boolean filter) {
+    if (x == 0
+        && y == 0
+        && width == src.getWidth()
+        && height == src.getHeight()
+        && (matrix == null || matrix.isIdentity())) {
+      return src; // Return the original.
+    }
+
+    if (x + width > src.getWidth()) {
+      throw new IllegalArgumentException("x + width must be <= bitmap.width()");
+    }
+    if (y + height > src.getHeight()) {
+      throw new IllegalArgumentException("y + height must be <= bitmap.height()");
+    }
+
+    Bitmap newBitmap = ReflectionHelpers.callConstructor(Bitmap.class);
+    ShadowLegacyBitmap shadowNewBitmap = Shadow.extract(newBitmap);
+
+    ShadowLegacyBitmap shadowSrcBitmap = Shadow.extract(src);
+    shadowNewBitmap.appendDescription(shadowSrcBitmap.getDescription());
+    shadowNewBitmap.appendDescription(" at (" + x + "," + y + ")");
+    shadowNewBitmap.appendDescription(" with width " + width + " and height " + height);
+
+    shadowNewBitmap.createdFromBitmap = src;
+    shadowNewBitmap.createdFromX = x;
+    shadowNewBitmap.createdFromY = y;
+    shadowNewBitmap.createdFromWidth = width;
+    shadowNewBitmap.createdFromHeight = height;
+    shadowNewBitmap.createdFromMatrix = matrix;
+    shadowNewBitmap.createdFromFilter = filter;
+    shadowNewBitmap.config = src.getConfig();
+    if (matrix != null) {
+      ShadowMatrix shadowMatrix = Shadow.extract(matrix);
+      shadowNewBitmap.appendDescription(" using matrix " + shadowMatrix.getDescription());
+
+      // Adjust width and height by using the matrix.
+      RectF mappedRect = new RectF();
+      matrix.mapRect(mappedRect, new RectF(0, 0, width, height));
+      width = Math.round(mappedRect.width());
+      height = Math.round(mappedRect.height());
+    }
+    if (filter) {
+      shadowNewBitmap.appendDescription(" with filter");
+    }
+
+    // updated if matrix is non-null
+    shadowNewBitmap.width = width;
+    shadowNewBitmap.height = height;
+    shadowNewBitmap.setMutable(true);
+    newBitmap.setDensity(src.getDensity());
+    if ((matrix == null || matrix.isIdentity()) && shadowSrcBitmap.bufferedImage != null) {
+      // Only simple cases are supported for setting image data to the new Bitmap.
+      shadowNewBitmap.bufferedImage =
+          shadowSrcBitmap.bufferedImage.getSubimage(x, y, width, height);
+    }
+    if (RuntimeEnvironment.getApiLevel() >= O) {
+      shadowNewBitmap.colorSpace = shadowSrcBitmap.colorSpace;
+    }
+    return newBitmap;
+  }
+
+  @Implementation
+  protected static Bitmap createBitmap(
+      int[] colors, int offset, int stride, int width, int height, Bitmap.Config config) {
+    return createBitmap(null, colors, offset, stride, width, height, config);
+  }
+
+  @Implementation(minSdk = JELLY_BEAN_MR1)
+  protected static Bitmap createBitmap(
+      DisplayMetrics displayMetrics,
+      int[] colors,
+      int offset,
+      int stride,
+      int width,
+      int height,
+      Bitmap.Config config) {
+    if (width <= 0) {
+      throw new IllegalArgumentException("width must be > 0");
+    }
+    if (height <= 0) {
+      throw new IllegalArgumentException("height must be > 0");
+    }
+    if (Math.abs(stride) < width) {
+      throw new IllegalArgumentException("abs(stride) must be >= width");
+    }
+    checkNotNull(config);
+    int lastScanline = offset + (height - 1) * stride;
+    int length = colors.length;
+    if (offset < 0
+        || (offset + width > length)
+        || lastScanline < 0
+        || (lastScanline + width > length)) {
+      throw new ArrayIndexOutOfBoundsException();
+    }
+
+    BufferedImage bufferedImage = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
+    bufferedImage.setRGB(0, 0, width, height, colors, offset, stride);
+    Bitmap bitmap = createBitmap(bufferedImage, width, height, config);
+    ShadowLegacyBitmap shadowBitmap = Shadow.extract(bitmap);
+    shadowBitmap.setMutable(false);
+    shadowBitmap.createdFromColors = colors;
+    if (displayMetrics != null) {
+      bitmap.setDensity(displayMetrics.densityDpi);
+    }
+    if (RuntimeEnvironment.getApiLevel() >= O) {
+      shadowBitmap.colorSpace = ColorSpace.get(ColorSpace.Named.SRGB);
+    }
+    return bitmap;
+  }
+
+  private static Bitmap createBitmap(
+      BufferedImage bufferedImage, int width, int height, Bitmap.Config config) {
+    Bitmap newBitmap = Bitmap.createBitmap(width, height, config);
+    ShadowLegacyBitmap shadowBitmap = Shadow.extract(newBitmap);
+    shadowBitmap.bufferedImage = bufferedImage;
+    return newBitmap;
+  }
+
+  @Implementation
+  protected static Bitmap createScaledBitmap(
+      Bitmap src, int dstWidth, int dstHeight, boolean filter) {
+    if (dstWidth == src.getWidth() && dstHeight == src.getHeight() && !filter) {
+      return src; // Return the original.
+    }
+    if (dstWidth <= 0 || dstHeight <= 0) {
+      throw new IllegalArgumentException("width and height must be > 0");
+    }
+    Bitmap scaledBitmap = ReflectionHelpers.callConstructor(Bitmap.class);
+    ShadowLegacyBitmap shadowBitmap = Shadow.extract(scaledBitmap);
+
+    ShadowLegacyBitmap shadowSrcBitmap = Shadow.extract(src);
+    shadowBitmap.appendDescription(shadowSrcBitmap.getDescription());
+    shadowBitmap.appendDescription(" scaled to " + dstWidth + " x " + dstHeight);
+    if (filter) {
+      shadowBitmap.appendDescription(" with filter " + filter);
+    }
+
+    shadowBitmap.createdFromBitmap = src;
+    shadowBitmap.scaledFromBitmap = src;
+    shadowBitmap.createdFromFilter = filter;
+    shadowBitmap.width = dstWidth;
+    shadowBitmap.height = dstHeight;
+    shadowBitmap.config = src.getConfig();
+    shadowBitmap.mutable = true;
+    if (!ImageUtil.scaledBitmap(src, scaledBitmap, filter)) {
+      shadowBitmap.bufferedImage =
+          new BufferedImage(dstWidth, dstHeight, BufferedImage.TYPE_INT_ARGB);
+      shadowBitmap.setPixelsInternal(
+          new int[shadowBitmap.getHeight() * shadowBitmap.getWidth()],
+          0,
+          0,
+          0,
+          0,
+          shadowBitmap.getWidth(),
+          shadowBitmap.getHeight());
+    }
+    if (RuntimeEnvironment.getApiLevel() >= O) {
+      shadowBitmap.colorSpace = shadowSrcBitmap.colorSpace;
+    }
+    return scaledBitmap;
+  }
+
+  @Implementation
+  protected static Bitmap nativeCreateFromParcel(Parcel p) {
+    int parceledWidth = p.readInt();
+    int parceledHeight = p.readInt();
+    Bitmap.Config parceledConfig = (Bitmap.Config) p.readSerializable();
+
+    int[] parceledColors = new int[parceledHeight * parceledWidth];
+    p.readIntArray(parceledColors);
+
+    return createBitmap(
+        parceledColors, 0, parceledWidth, parceledWidth, parceledHeight, parceledConfig);
+  }
+
+  static int getBytesPerPixel(Bitmap.Config config) {
+    if (config == null) {
+      throw new NullPointerException("Bitmap config was null.");
+    }
+    switch (config) {
+      case RGBA_F16:
+        return 8;
+      case ARGB_8888:
+      case HARDWARE:
+        return 4;
+      case RGB_565:
+      case ARGB_4444:
+        return 2;
+      case ALPHA_8:
+        return 1;
+      default:
+        throw new IllegalArgumentException("Unknown bitmap config: " + config);
+    }
+  }
+
+  /**
+   * Reference to original Bitmap from which this Bitmap was created. {@code null} if this Bitmap
+   * was not copied from another instance.
+   *
+   * @return Original Bitmap from which this Bitmap was created.
+   */
+  @Override
+  public Bitmap getCreatedFromBitmap() {
+    return createdFromBitmap;
+  }
+
+  /**
+   * Resource ID from which this Bitmap was created. {@code 0} if this Bitmap was not created from a
+   * resource.
+   *
+   * @return Resource ID from which this Bitmap was created.
+   */
+  @Override
+  public int getCreatedFromResId() {
+    return createdFromResId;
+  }
+
+  /**
+   * Path from which this Bitmap was created. {@code null} if this Bitmap was not create from a
+   * path.
+   *
+   * @return Path from which this Bitmap was created.
+   */
+  @Override
+  public String getCreatedFromPath() {
+    return createdFromPath;
+  }
+
+  /**
+   * {@link InputStream} from which this Bitmap was created. {@code null} if this Bitmap was not
+   * created from a stream.
+   *
+   * @return InputStream from which this Bitmap was created.
+   */
+  @Override
+  public InputStream getCreatedFromStream() {
+    return createdFromStream;
+  }
+
+  /**
+   * Bytes from which this Bitmap was created. {@code null} if this Bitmap was not created from
+   * bytes.
+   *
+   * @return Bytes from which this Bitmap was created.
+   */
+  @Override
+  public byte[] getCreatedFromBytes() {
+    return createdFromBytes;
+  }
+
+  /**
+   * Horizontal offset within {@link #getCreatedFromBitmap()} of this Bitmap's content, or -1.
+   *
+   * @return Horizontal offset within {@link #getCreatedFromBitmap()}.
+   */
+  @Override
+  public int getCreatedFromX() {
+    return createdFromX;
+  }
+
+  /**
+   * Vertical offset within {@link #getCreatedFromBitmap()} of this Bitmap's content, or -1.
+   *
+   * @return Vertical offset within {@link #getCreatedFromBitmap()} of this Bitmap's content, or -1.
+   */
+  @Override
+  public int getCreatedFromY() {
+    return createdFromY;
+  }
+
+  /**
+   * Width from {@link #getCreatedFromX()} within {@link #getCreatedFromBitmap()} of this Bitmap's
+   * content, or -1.
+   *
+   * @return Width from {@link #getCreatedFromX()} within {@link #getCreatedFromBitmap()} of this
+   *     Bitmap's content, or -1.
+   */
+  @Override
+  public int getCreatedFromWidth() {
+    return createdFromWidth;
+  }
+
+  /**
+   * Height from {@link #getCreatedFromX()} within {@link #getCreatedFromBitmap()} of this Bitmap's
+   * content, or -1.
+   *
+   * @return Height from {@link #getCreatedFromX()} within {@link #getCreatedFromBitmap()} of this
+   *     Bitmap's content, or -1.
+   */
+  @Override
+  public int getCreatedFromHeight() {
+    return createdFromHeight;
+  }
+
+  /**
+   * Color array from which this Bitmap was created. {@code null} if this Bitmap was not created
+   * from a color array.
+   *
+   * @return Color array from which this Bitmap was created.
+   */
+  @Override
+  public int[] getCreatedFromColors() {
+    return createdFromColors;
+  }
+
+  /**
+   * Matrix from which this Bitmap's content was transformed, or {@code null}.
+   *
+   * @return Matrix from which this Bitmap's content was transformed, or {@code null}.
+   */
+  @Override
+  public Matrix getCreatedFromMatrix() {
+    return createdFromMatrix;
+  }
+
+  /**
+   * {@code true} if this Bitmap was created with filtering.
+   *
+   * @return {@code true} if this Bitmap was created with filtering.
+   */
+  @Override
+  public boolean getCreatedFromFilter() {
+    return createdFromFilter;
+  }
+
+  @Implementation(minSdk = S)
+  protected Bitmap asShared() {
+    setMutable(false);
+    return realBitmap;
+  }
+
+  @Implementation
+  protected boolean compress(Bitmap.CompressFormat format, int quality, OutputStream stream) {
+    appendDescription(" compressed as " + format + " with quality " + quality);
+    return ImageUtil.writeToStream(realBitmap, format, quality, stream);
+  }
+
+  @Implementation
+  protected void setPixels(
+      int[] pixels, int offset, int stride, int x, int y, int width, int height) {
+    checkBitmapMutable();
+    setPixelsInternal(pixels, offset, stride, x, y, width, height);
+  }
+
+  void setPixelsInternal(
+      int[] pixels, int offset, int stride, int x, int y, int width, int height) {
+    if (bufferedImage == null) {
+      bufferedImage = new BufferedImage(getWidth(), getHeight(), BufferedImage.TYPE_INT_ARGB);
+    }
+    bufferedImage.setRGB(x, y, width, height, pixels, offset, stride);
+  }
+
+  @Implementation
+  protected int getPixel(int x, int y) {
+    internalCheckPixelAccess(x, y);
+    if (bufferedImage != null) {
+      // Note that getPixel() returns a non-premultiplied ARGB value; if
+      // config is RGB_565, our return value will likely be more precise than
+      // on a physical device, since it needs to map each color component from
+      // 5 or 6 bits to 8 bits.
+      return bufferedImage.getRGB(x, y);
+    } else {
+      return 0;
+    }
+  }
+
+  @Implementation
+  protected void setPixel(int x, int y, int color) {
+    checkBitmapMutable();
+    internalCheckPixelAccess(x, y);
+    if (bufferedImage == null) {
+      bufferedImage = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
+    }
+    bufferedImage.setRGB(x, y, color);
+  }
+
+  /**
+   * Note that this method will return a RuntimeException unless: - {@code pixels} has the same
+   * length as the number of pixels of the bitmap. - {@code x = 0} - {@code y = 0} - {@code width}
+   * and {@code height} height match the current bitmap's dimensions.
+   */
+  @Implementation
+  protected void getPixels(
+      int[] pixels, int offset, int stride, int x, int y, int width, int height) {
+    bufferedImage.getRGB(x, y, width, height, pixels, offset, stride);
+  }
+
+  @Implementation
+  protected int getRowBytes() {
+    return getBytesPerPixel(config) * getWidth();
+  }
+
+  @Implementation
+  protected int getByteCount() {
+    return getRowBytes() * getHeight();
+  }
+
+  @Implementation
+  protected void recycle() {
+    recycled = true;
+  }
+
+  @Implementation
+  protected final boolean isRecycled() {
+    return recycled;
+  }
+
+  @Implementation
+  protected Bitmap copy(Bitmap.Config config, boolean isMutable) {
+    Bitmap newBitmap = ReflectionHelpers.callConstructor(Bitmap.class);
+    ShadowLegacyBitmap shadowBitmap = Shadow.extract(newBitmap);
+    shadowBitmap.createdFromBitmap = realBitmap;
+    shadowBitmap.config = config;
+    shadowBitmap.mutable = isMutable;
+    shadowBitmap.height = getHeight();
+    shadowBitmap.width = getWidth();
+    if (bufferedImage != null) {
+      ColorModel cm = bufferedImage.getColorModel();
+      WritableRaster raster =
+          bufferedImage.copyData(bufferedImage.getRaster().createCompatibleWritableRaster());
+      shadowBitmap.bufferedImage = new BufferedImage(cm, raster, false, null);
+    }
+    return newBitmap;
+  }
+
+  @Implementation(minSdk = KITKAT)
+  protected final int getAllocationByteCount() {
+    return getRowBytes() * getHeight();
+  }
+
+  @Implementation
+  protected final Bitmap.Config getConfig() {
+    return config;
+  }
+
+  @Implementation(minSdk = KITKAT)
+  protected void setConfig(Bitmap.Config config) {
+    this.config = config;
+  }
+
+  @Implementation
+  protected final boolean isMutable() {
+    return mutable;
+  }
+
+  @Override
+  public void setMutable(boolean mutable) {
+    this.mutable = mutable;
+  }
+
+  @Override
+  public void appendDescription(String s) {
+    description += s;
+  }
+
+  @Override
+  public String getDescription() {
+    return description;
+  }
+
+  @Override
+  public void setDescription(String s) {
+    description = s;
+  }
+
+  @Implementation
+  protected final boolean hasAlpha() {
+    return hasAlpha && config != Bitmap.Config.RGB_565;
+  }
+
+  @Implementation
+  protected void setHasAlpha(boolean hasAlpha) {
+    this.hasAlpha = hasAlpha;
+  }
+
+  @Implementation
+  protected Bitmap extractAlpha() {
+    WritableRaster raster = bufferedImage.getAlphaRaster();
+    BufferedImage alphaImage = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
+    alphaImage.getAlphaRaster().setRect(raster);
+    return createBitmap(alphaImage, getWidth(), getHeight(), Bitmap.Config.ALPHA_8);
+  }
+
+  /**
+   * This shadow implementation ignores the given paint and offsetXY and simply calls {@link
+   * #extractAlpha()}.
+   */
+  @Implementation
+  protected Bitmap extractAlpha(Paint paint, int[] offsetXY) {
+    return extractAlpha();
+  }
+
+  @Implementation(minSdk = JELLY_BEAN_MR1)
+  protected final boolean hasMipMap() {
+    return hasMipMap;
+  }
+
+  @Implementation(minSdk = JELLY_BEAN_MR1)
+  protected final void setHasMipMap(boolean hasMipMap) {
+    this.hasMipMap = hasMipMap;
+  }
+
+  @Implementation
+  protected int getWidth() {
+    return width;
+  }
+
+  @Implementation(minSdk = KITKAT)
+  protected void setWidth(int width) {
+    this.width = width;
+  }
+
+  @Implementation
+  protected int getHeight() {
+    return height;
+  }
+
+  @Implementation(minSdk = KITKAT)
+  protected void setHeight(int height) {
+    this.height = height;
+  }
+
+  @Implementation
+  protected int getGenerationId() {
+    return 0;
+  }
+
+  @Implementation(minSdk = M)
+  protected Bitmap createAshmemBitmap() {
+    return realBitmap;
+  }
+
+  @Implementation
+  protected void eraseColor(int color) {
+    if (bufferedImage != null) {
+      int[] pixels = ((DataBufferInt) bufferedImage.getRaster().getDataBuffer()).getData();
+      Arrays.fill(pixels, color);
+    }
+    setDescription(String.format("Bitmap (%d, %d)", width, height));
+    if (color != 0) {
+      appendDescription(String.format(" erased with 0x%08x", color));
+    }
+  }
+
+  @Implementation
+  protected void writeToParcel(Parcel p, int flags) {
+    p.writeInt(width);
+    p.writeInt(height);
+    p.writeSerializable(config);
+    int[] pixels = new int[width * height];
+    getPixels(pixels, 0, width, 0, 0, width, height);
+    p.writeIntArray(pixels);
+  }
+
+  @Implementation
+  protected void copyPixelsFromBuffer(Buffer dst) {
+    if (isRecycled()) {
+      throw new IllegalStateException("Can't call copyPixelsFromBuffer() on a recycled bitmap");
+    }
+
+    // See the related comment in #copyPixelsToBuffer(Buffer).
+    if (getBytesPerPixel(config) != INTERNAL_BYTES_PER_PIXEL) {
+      throw new RuntimeException(
+          "Not implemented: only Bitmaps with "
+              + INTERNAL_BYTES_PER_PIXEL
+              + " bytes per pixel are supported");
+    }
+    if (!(dst instanceof ByteBuffer) && !(dst instanceof IntBuffer)) {
+      throw new RuntimeException("Not implemented: unsupported Buffer subclass");
+    }
+
+    ByteBuffer byteBuffer = null;
+    IntBuffer intBuffer;
+    if (dst instanceof IntBuffer) {
+      intBuffer = (IntBuffer) dst;
+    } else {
+      byteBuffer = (ByteBuffer) dst;
+      intBuffer = byteBuffer.asIntBuffer();
+    }
+
+    if (intBuffer.remaining() < (width * height)) {
+      throw new RuntimeException("Buffer not large enough for pixels");
+    }
+
+    int[] colors = new int[width * height];
+    intBuffer.get(colors);
+    if (byteBuffer != null) {
+      byteBuffer.position(byteBuffer.position() + intBuffer.position() * INTERNAL_BYTES_PER_PIXEL);
+    }
+    int[] pixels = ((DataBufferInt) bufferedImage.getRaster().getDataBuffer()).getData();
+    System.arraycopy(colors, 0, pixels, 0, pixels.length);
+  }
+
+  @Implementation
+  protected void copyPixelsToBuffer(Buffer dst) {
+    // Ensure that the Bitmap uses 4 bytes per pixel, since we always use 4 bytes per pixels
+    // internally. Clients of this API probably expect that the buffer size must be >=
+    // getByteCount(), but if we don't enforce this restriction then for RGB_4444 and other
+    // configs that value would be smaller then the buffer size we actually need.
+    if (getBytesPerPixel(config) != INTERNAL_BYTES_PER_PIXEL) {
+      throw new RuntimeException(
+          "Not implemented: only Bitmaps with "
+              + INTERNAL_BYTES_PER_PIXEL
+              + " bytes per pixel are supported");
+    }
+
+    if (!(dst instanceof ByteBuffer) && !(dst instanceof IntBuffer)) {
+      throw new RuntimeException("Not implemented: unsupported Buffer subclass");
+    }
+    int[] pixels = ((DataBufferInt) bufferedImage.getRaster().getDataBuffer()).getData();
+    if (dst instanceof ByteBuffer) {
+      IntBuffer intBuffer = ((ByteBuffer) dst).asIntBuffer();
+      intBuffer.put(pixels);
+      dst.position(intBuffer.position() * 4);
+    } else if (dst instanceof IntBuffer) {
+      ((IntBuffer) dst).put(pixels);
+    }
+  }
+
+  @Implementation(minSdk = KITKAT)
+  protected void reconfigure(int width, int height, Bitmap.Config config) {
+    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && this.config == Bitmap.Config.HARDWARE) {
+      throw new IllegalStateException("native-backed bitmaps may not be reconfigured");
+    }
+
+    // This should throw if the resulting allocation size is greater than the initial allocation
+    // size of our Bitmap, but we don't keep track of that information reliably, so we're forced to
+    // assume that our original dimensions and config are large enough to fit the new dimensions and
+    // config
+    this.width = width;
+    this.height = height;
+    this.config = config;
+    bufferedImage = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
+  }
+
+  @Implementation(minSdk = KITKAT)
+  protected boolean isPremultiplied() {
+    return requestPremultiplied && hasAlpha();
+  }
+
+  @Implementation(minSdk = KITKAT)
+  protected void setPremultiplied(boolean isPremultiplied) {
+    this.requestPremultiplied = isPremultiplied;
+  }
+
+  @Implementation(minSdk = O)
+  protected ColorSpace getColorSpace() {
+    return colorSpace;
+  }
+
+  @Implementation(minSdk = Q)
+  protected void setColorSpace(ColorSpace colorSpace) {
+    this.colorSpace = checkNotNull(colorSpace);
+  }
+
+  @Implementation
+  protected boolean sameAs(Bitmap other) {
+    if (other == null) {
+      return false;
+    }
+    ShadowLegacyBitmap shadowOtherBitmap = Shadow.extract(other);
+    if (this.width != shadowOtherBitmap.width || this.height != shadowOtherBitmap.height) {
+      return false;
+    }
+    if (this.config != shadowOtherBitmap.config) {
+      return false;
+    }
+
+    if (bufferedImage == null && shadowOtherBitmap.bufferedImage != null) {
+      return false;
+    } else if (bufferedImage != null && shadowOtherBitmap.bufferedImage == null) {
+      return false;
+    } else if (bufferedImage != null && shadowOtherBitmap.bufferedImage != null) {
+      int[] pixels = ((DataBufferInt) bufferedImage.getData().getDataBuffer()).getData();
+      int[] otherPixels =
+          ((DataBufferInt) shadowOtherBitmap.bufferedImage.getData().getDataBuffer()).getData();
+      if (!Arrays.equals(pixels, otherPixels)) {
+        return false;
+      }
+    }
+    // When Bitmap.createScaledBitmap is called, the colors array is cleared, so we need a basic
+    // way to detect if two scaled bitmaps are the same.
+    if (scaledFromBitmap != null && shadowOtherBitmap.scaledFromBitmap != null) {
+      return scaledFromBitmap.sameAs(shadowOtherBitmap.scaledFromBitmap);
+    }
+    return true;
+  }
+
+  void setCreatedFromResId(int resId, String description) {
+    this.createdFromResId = resId;
+    appendDescription(" for resource:" + description);
+  }
+
+  private void checkBitmapMutable() {
+    if (isRecycled()) {
+      throw new IllegalStateException("Can't call setPixel() on a recycled bitmap");
+    } else if (!isMutable()) {
+      throw new IllegalStateException("Bitmap is immutable");
+    }
+  }
+
+  private void internalCheckPixelAccess(int x, int y) {
+    if (x < 0) {
+      throw new IllegalArgumentException("x must be >= 0");
+    }
+    if (y < 0) {
+      throw new IllegalArgumentException("y must be >= 0");
+    }
+    if (x >= getWidth()) {
+      throw new IllegalArgumentException("x must be < bitmap.width()");
+    }
+    if (y >= getHeight()) {
+      throw new IllegalArgumentException("y must be < bitmap.height()");
+    }
+  }
+
+  void drawRect(Rect r, Paint paint) {
+    if (bufferedImage == null) {
+      return;
+    }
+    int[] pixels = ((DataBufferInt) bufferedImage.getRaster().getDataBuffer()).getData();
+
+    Rect toDraw =
+        new Rect(
+            max(0, r.left), max(0, r.top), min(getWidth(), r.right), min(getHeight(), r.bottom));
+    if (toDraw.left == 0 && toDraw.top == 0 && toDraw.right == getWidth()) {
+      Arrays.fill(pixels, 0, getWidth() * toDraw.bottom, paint.getColor());
+      return;
+    }
+    for (int y = toDraw.top; y < toDraw.bottom; y++) {
+      Arrays.fill(
+          pixels, y * getWidth() + toDraw.left, y * getWidth() + toDraw.right, paint.getColor());
+    }
+  }
+
+  void drawRect(RectF r, Paint paint) {
+    if (bufferedImage == null) {
+      return;
+    }
+
+    Graphics2D graphics2D = bufferedImage.createGraphics();
+    Rectangle2D r2d = new Rectangle2D.Float(r.left, r.top, r.right - r.left, r.bottom - r.top);
+    graphics2D.setColor(new Color(paint.getColor()));
+    graphics2D.draw(r2d);
+    graphics2D.dispose();
+  }
+
+  void drawBitmap(Bitmap source, int left, int top) {
+    ShadowLegacyBitmap shadowSource = Shadow.extract(source);
+    if (bufferedImage == null || shadowSource.bufferedImage == null) {
+      // pixel data not available, so there's nothing we can do
+      return;
+    }
+
+    int[] pixels = ((DataBufferInt) bufferedImage.getRaster().getDataBuffer()).getData();
+    int[] sourcePixels =
+        ((DataBufferInt) shadowSource.bufferedImage.getRaster().getDataBuffer()).getData();
+
+    // fast path
+    if (left == 0 && top == 0 && getWidth() == source.getWidth()) {
+      int size = min(getWidth() * getHeight(), source.getWidth() * source.getHeight());
+      System.arraycopy(sourcePixels, 0, pixels, 0, size);
+      return;
+    }
+    // slower (row-by-row) path
+    int startSourceY = max(0, -top);
+    int startSourceX = max(0, -left);
+    int startY = max(0, top);
+    int startX = max(0, left);
+    int endY = min(getHeight(), top + source.getHeight());
+    int endX = min(getWidth(), left + source.getWidth());
+    int lenY = endY - startY;
+    int lenX = endX - startX;
+    for (int y = 0; y < lenY; y++) {
+      System.arraycopy(
+          sourcePixels,
+          (startSourceY + y) * source.getWidth() + startSourceX,
+          pixels,
+          (startY + y) * getWidth() + startX,
+          lenX);
+    }
+  }
+
+  BufferedImage getBufferedImage() {
+    return bufferedImage;
+  }
+
+  void setBufferedImage(BufferedImage bufferedImage) {
+    this.bufferedImage = bufferedImage;
+  }
+}
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowLegacyCanvas.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowLegacyCanvas.java
new file mode 100644
index 0000000..9c63ed3
--- /dev/null
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowLegacyCanvas.java
@@ -0,0 +1,642 @@
+package org.robolectric.shadows;
+
+import static android.os.Build.VERSION_CODES.KITKAT;
+import static android.os.Build.VERSION_CODES.KITKAT_WATCH;
+import static android.os.Build.VERSION_CODES.LOLLIPOP;
+import static android.os.Build.VERSION_CODES.LOLLIPOP_MR1;
+import static android.os.Build.VERSION_CODES.M;
+import static android.os.Build.VERSION_CODES.N_MR1;
+import static android.os.Build.VERSION_CODES.O;
+import static android.os.Build.VERSION_CODES.P;
+import static android.os.Build.VERSION_CODES.Q;
+import static android.os.Build.VERSION_CODES.R;
+import static android.os.Build.VERSION_CODES.S;
+
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.ColorFilter;
+import android.graphics.Matrix;
+import android.graphics.Paint;
+import android.graphics.Path;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import com.google.common.base.Preconditions;
+import java.util.ArrayList;
+import java.util.List;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.annotation.Implementation;
+import org.robolectric.annotation.Implements;
+import org.robolectric.annotation.RealObject;
+import org.robolectric.annotation.ReflectorObject;
+import org.robolectric.annotation.Resetter;
+import org.robolectric.res.android.NativeObjRegistry;
+import org.robolectric.shadow.api.Shadow;
+import org.robolectric.util.ReflectionHelpers;
+import org.robolectric.util.reflector.Direct;
+import org.robolectric.util.reflector.ForType;
+
+/**
+ * Broken. This implementation is very specific to the application for which it was developed. Todo:
+ * Reimplement. Consider using the same strategy of collecting a history of draw events and
+ * providing methods for writing queries based on type, number, and order of events.
+ */
+@SuppressWarnings({"UnusedDeclaration"})
+@Implements(value = Canvas.class, isInAndroidSdk = false)
+public class ShadowLegacyCanvas extends ShadowCanvas {
+  private static final NativeObjRegistry<NativeCanvas> nativeObjectRegistry =
+      new NativeObjRegistry<>(NativeCanvas.class);
+
+  @RealObject protected Canvas realCanvas;
+  @ReflectorObject protected CanvasReflector canvasReflector;
+
+  private final List<RoundRectPaintHistoryEvent> roundRectPaintEvents = new ArrayList<>();
+  private List<PathPaintHistoryEvent> pathPaintEvents = new ArrayList<>();
+  private List<CirclePaintHistoryEvent> circlePaintEvents = new ArrayList<>();
+  private List<ArcPaintHistoryEvent> arcPaintEvents = new ArrayList<>();
+  private List<RectPaintHistoryEvent> rectPaintEvents = new ArrayList<>();
+  private List<LinePaintHistoryEvent> linePaintEvents = new ArrayList<>();
+  private List<OvalPaintHistoryEvent> ovalPaintEvents = new ArrayList<>();
+  private List<TextHistoryEvent> drawnTextEventHistory = new ArrayList<>();
+  private Paint drawnPaint;
+  private Bitmap targetBitmap = ReflectionHelpers.callConstructor(Bitmap.class);
+  private float translateX;
+  private float translateY;
+  private float scaleX = 1;
+  private float scaleY = 1;
+  private int height;
+  private int width;
+
+  @Implementation
+  protected void __constructor__(Bitmap bitmap) {
+    canvasReflector.__constructor__(bitmap);
+    this.targetBitmap = bitmap;
+  }
+
+  private long getNativeId() {
+    return RuntimeEnvironment.getApiLevel() <= KITKAT_WATCH
+        ? (int) ReflectionHelpers.getField(realCanvas, "mNativeCanvas")
+        : realCanvas.getNativeCanvasWrapper();
+  }
+
+  private NativeCanvas getNativeCanvas() {
+    return nativeObjectRegistry.getNativeObject(getNativeId());
+  }
+
+  @Override
+  public void appendDescription(String s) {
+    ShadowBitmap shadowBitmap = Shadow.extract(targetBitmap);
+    shadowBitmap.appendDescription(s);
+  }
+
+  @Override
+  public String getDescription() {
+    ShadowBitmap shadowBitmap = Shadow.extract(targetBitmap);
+    return shadowBitmap.getDescription();
+  }
+
+  @Implementation
+  protected void setBitmap(Bitmap bitmap) {
+    targetBitmap = bitmap;
+  }
+
+  @Implementation
+  protected void drawText(String text, float x, float y, Paint paint) {
+    drawnTextEventHistory.add(new TextHistoryEvent(x, y, paint, text));
+  }
+
+  @Implementation
+  protected void drawText(CharSequence text, int start, int end, float x, float y, Paint paint) {
+    drawnTextEventHistory.add(
+        new TextHistoryEvent(x, y, paint, text.subSequence(start, end).toString()));
+  }
+
+  @Implementation
+  protected void drawText(char[] text, int index, int count, float x, float y, Paint paint) {
+    drawnTextEventHistory.add(new TextHistoryEvent(x, y, paint, new String(text, index, count)));
+  }
+
+  @Implementation
+  protected void drawText(String text, int start, int end, float x, float y, Paint paint) {
+    drawnTextEventHistory.add(new TextHistoryEvent(x, y, paint, text.substring(start, end)));
+  }
+
+  @Implementation
+  protected void translate(float x, float y) {
+    this.translateX = x;
+    this.translateY = y;
+  }
+
+  @Implementation
+  protected void scale(float sx, float sy) {
+    this.scaleX = sx;
+    this.scaleY = sy;
+  }
+
+  @Implementation
+  protected void scale(float sx, float sy, float px, float py) {
+    this.scaleX = sx;
+    this.scaleY = sy;
+  }
+
+  @Implementation
+  protected void drawPaint(Paint paint) {
+    drawnPaint = paint;
+  }
+
+  @Implementation
+  protected void drawColor(int color) {
+    appendDescription("draw color " + color);
+  }
+
+  @Implementation
+  protected void drawBitmap(Bitmap bitmap, float left, float top, Paint paint) {
+    describeBitmap(bitmap, paint);
+
+    int x = (int) (left + translateX);
+    int y = (int) (top + translateY);
+    if (x != 0 || y != 0) {
+      appendDescription(" at (" + x + "," + y + ")");
+    }
+
+    if (scaleX != 1 && scaleY != 1) {
+      appendDescription(" scaled by (" + scaleX + "," + scaleY + ")");
+    }
+
+    if (bitmap != null && targetBitmap != null) {
+      ShadowLegacyBitmap shadowTargetBitmap = Shadow.extract(targetBitmap);
+      shadowTargetBitmap.drawBitmap(bitmap, (int) left, (int) top);
+    }
+  }
+
+  @Implementation
+  protected void drawBitmap(Bitmap bitmap, Rect src, Rect dst, Paint paint) {
+    describeBitmap(bitmap, paint);
+
+    StringBuilder descriptionBuilder = new StringBuilder();
+    if (dst != null) {
+      descriptionBuilder
+          .append(" at (")
+          .append(dst.left)
+          .append(",")
+          .append(dst.top)
+          .append(") with height=")
+          .append(dst.height())
+          .append(" and width=")
+          .append(dst.width());
+    }
+
+    if (src != null) {
+      descriptionBuilder.append(" taken from ").append(src.toString());
+    }
+    appendDescription(descriptionBuilder.toString());
+  }
+
+  @Implementation
+  protected void drawBitmap(Bitmap bitmap, Rect src, RectF dst, Paint paint) {
+    describeBitmap(bitmap, paint);
+
+    StringBuilder descriptionBuilder = new StringBuilder();
+    if (dst != null) {
+      descriptionBuilder
+          .append(" at (")
+          .append(dst.left)
+          .append(",")
+          .append(dst.top)
+          .append(") with height=")
+          .append(dst.height())
+          .append(" and width=")
+          .append(dst.width());
+    }
+
+    if (src != null) {
+      descriptionBuilder.append(" taken from ").append(src.toString());
+    }
+    appendDescription(descriptionBuilder.toString());
+  }
+
+  @Implementation
+  protected void drawBitmap(Bitmap bitmap, Matrix matrix, Paint paint) {
+    describeBitmap(bitmap, paint);
+
+    ShadowMatrix shadowMatrix = Shadow.extract(matrix);
+    appendDescription(" transformed by " + shadowMatrix.getDescription());
+  }
+
+  @Implementation
+  protected void drawPath(Path path, Paint paint) {
+    pathPaintEvents.add(new PathPaintHistoryEvent(new Path(path), new Paint(paint)));
+
+    separateLines();
+    ShadowPath shadowPath = Shadow.extract(path);
+    appendDescription("Path " + shadowPath.getPoints().toString());
+  }
+
+  @Implementation
+  protected void drawCircle(float cx, float cy, float radius, Paint paint) {
+    circlePaintEvents.add(new CirclePaintHistoryEvent(cx, cy, radius, paint));
+  }
+
+  @Implementation
+  protected void drawArc(
+      RectF oval, float startAngle, float sweepAngle, boolean useCenter, Paint paint) {
+    arcPaintEvents.add(new ArcPaintHistoryEvent(oval, startAngle, sweepAngle, useCenter, paint));
+  }
+
+  @Implementation
+  protected void drawRect(float left, float top, float right, float bottom, Paint paint) {
+    rectPaintEvents.add(new RectPaintHistoryEvent(left, top, right, bottom, paint));
+
+    if (targetBitmap != null) {
+      ShadowLegacyBitmap shadowTargetBitmap = Shadow.extract(targetBitmap);
+      shadowTargetBitmap.drawRect(new RectF(left, top, right, bottom), paint);
+    }
+  }
+
+  @Implementation
+  protected void drawRect(Rect r, Paint paint) {
+    rectPaintEvents.add(new RectPaintHistoryEvent(r.left, r.top, r.right, r.bottom, paint));
+
+    if (targetBitmap != null) {
+      ShadowLegacyBitmap shadowTargetBitmap = Shadow.extract(targetBitmap);
+      shadowTargetBitmap.drawRect(r, paint);
+    }
+  }
+
+  @Implementation
+  protected void drawRoundRect(RectF rect, float rx, float ry, Paint paint) {
+    roundRectPaintEvents.add(
+        new RoundRectPaintHistoryEvent(
+            rect.left, rect.top, rect.right, rect.bottom, rx, ry, paint));
+  }
+
+  @Implementation
+  protected void drawLine(float startX, float startY, float stopX, float stopY, Paint paint) {
+    linePaintEvents.add(new LinePaintHistoryEvent(startX, startY, stopX, stopY, paint));
+  }
+
+  @Implementation
+  protected void drawOval(RectF oval, Paint paint) {
+    ovalPaintEvents.add(new OvalPaintHistoryEvent(oval, paint));
+  }
+
+  private void describeBitmap(Bitmap bitmap, Paint paint) {
+    separateLines();
+
+    ShadowBitmap shadowBitmap = Shadow.extract(bitmap);
+    appendDescription(shadowBitmap.getDescription());
+
+    if (paint != null) {
+      ColorFilter colorFilter = paint.getColorFilter();
+      if (colorFilter != null) {
+        appendDescription(" with " + colorFilter.getClass().getSimpleName());
+      }
+    }
+  }
+
+  private void separateLines() {
+    if (getDescription().length() != 0) {
+      appendDescription("\n");
+    }
+  }
+
+  @Override
+  public int getPathPaintHistoryCount() {
+    return pathPaintEvents.size();
+  }
+
+  @Override
+  public int getCirclePaintHistoryCount() {
+    return circlePaintEvents.size();
+  }
+
+  @Override
+  public int getArcPaintHistoryCount() {
+    return arcPaintEvents.size();
+  }
+
+  @Override
+  public boolean hasDrawnPath() {
+    return getPathPaintHistoryCount() > 0;
+  }
+
+  @Override
+  public boolean hasDrawnCircle() {
+    return circlePaintEvents.size() > 0;
+  }
+
+  @Override
+  public Paint getDrawnPathPaint(int i) {
+    return pathPaintEvents.get(i).pathPaint;
+  }
+
+  @Override
+  public Path getDrawnPath(int i) {
+    return pathPaintEvents.get(i).drawnPath;
+  }
+
+  @Override
+  public CirclePaintHistoryEvent getDrawnCircle(int i) {
+    return circlePaintEvents.get(i);
+  }
+
+  @Override
+  public ArcPaintHistoryEvent getDrawnArc(int i) {
+    return arcPaintEvents.get(i);
+  }
+
+  @Override
+  public void resetCanvasHistory() {
+    drawnTextEventHistory.clear();
+    pathPaintEvents.clear();
+    circlePaintEvents.clear();
+    rectPaintEvents.clear();
+    roundRectPaintEvents.clear();
+    linePaintEvents.clear();
+    ovalPaintEvents.clear();
+    ShadowBitmap shadowBitmap = Shadow.extract(targetBitmap);
+    shadowBitmap.setDescription("");
+  }
+
+  @Override
+  public Paint getDrawnPaint() {
+    return drawnPaint;
+  }
+
+  @Override
+  public void setHeight(int height) {
+    this.height = height;
+  }
+
+  @Override
+  public void setWidth(int width) {
+    this.width = width;
+  }
+
+  @Implementation
+  protected int getWidth() {
+    if (width == 0) {
+      return targetBitmap.getWidth();
+    }
+    return width;
+  }
+
+  @Implementation
+  protected int getHeight() {
+    if (height == 0) {
+      return targetBitmap.getHeight();
+    }
+    return height;
+  }
+
+  @Implementation
+  protected boolean getClipBounds(Rect bounds) {
+    Preconditions.checkNotNull(bounds);
+    if (targetBitmap == null) {
+      return false;
+    }
+    bounds.set(0, 0, targetBitmap.getWidth(), targetBitmap.getHeight());
+    return !bounds.isEmpty();
+  }
+
+  @Override
+  public TextHistoryEvent getDrawnTextEvent(int i) {
+    return drawnTextEventHistory.get(i);
+  }
+
+  @Override
+  public int getTextHistoryCount() {
+    return drawnTextEventHistory.size();
+  }
+
+  @Override
+  public RectPaintHistoryEvent getDrawnRect(int i) {
+    return rectPaintEvents.get(i);
+  }
+
+  @Override
+  public RectPaintHistoryEvent getLastDrawnRect() {
+    return rectPaintEvents.get(rectPaintEvents.size() - 1);
+  }
+
+  @Override
+  public int getRectPaintHistoryCount() {
+    return rectPaintEvents.size();
+  }
+
+  @Override
+  public RoundRectPaintHistoryEvent getDrawnRoundRect(int i) {
+    return roundRectPaintEvents.get(i);
+  }
+
+  @Override
+  public RoundRectPaintHistoryEvent getLastDrawnRoundRect() {
+    return roundRectPaintEvents.get(roundRectPaintEvents.size() - 1);
+  }
+
+  @Override
+  public int getRoundRectPaintHistoryCount() {
+    return roundRectPaintEvents.size();
+  }
+
+  @Override
+  public LinePaintHistoryEvent getDrawnLine(int i) {
+    return linePaintEvents.get(i);
+  }
+
+  @Override
+  public int getLinePaintHistoryCount() {
+    return linePaintEvents.size();
+  }
+
+  @Override
+  public int getOvalPaintHistoryCount() {
+    return ovalPaintEvents.size();
+  }
+
+  @Override
+  public OvalPaintHistoryEvent getDrawnOval(int i) {
+    return ovalPaintEvents.get(i);
+  }
+
+  @Implementation(maxSdk = N_MR1)
+  protected int save() {
+    return getNativeCanvas().save();
+  }
+
+  @Implementation(maxSdk = N_MR1)
+  protected void restore() {
+    getNativeCanvas().restore();
+  }
+
+  @Implementation(maxSdk = N_MR1)
+  protected int getSaveCount() {
+    return getNativeCanvas().getSaveCount();
+  }
+
+  @Implementation(maxSdk = N_MR1)
+  protected void restoreToCount(int saveCount) {
+    getNativeCanvas().restoreToCount(saveCount);
+  }
+
+  @Implementation(minSdk = KITKAT)
+  protected void release() {
+    nativeObjectRegistry.unregister(getNativeId());
+    canvasReflector.release();
+  }
+
+  @Implementation(maxSdk = KITKAT_WATCH)
+  protected static int initRaster(int bitmapHandle) {
+    return (int) nativeObjectRegistry.register(new NativeCanvas());
+  }
+
+  @Implementation(minSdk = LOLLIPOP, maxSdk = LOLLIPOP_MR1)
+  protected static long initRaster(long bitmapHandle) {
+    return nativeObjectRegistry.register(new NativeCanvas());
+  }
+
+  @Implementation(minSdk = M, maxSdk = N_MR1)
+  protected static long initRaster(Bitmap bitmap) {
+    return nativeObjectRegistry.register(new NativeCanvas());
+  }
+
+  @Implementation(minSdk = O, maxSdk = P)
+  protected static long nInitRaster(Bitmap bitmap) {
+    return nativeObjectRegistry.register(new NativeCanvas());
+  }
+
+  @Implementation(minSdk = Q)
+  protected static long nInitRaster(long bitmapHandle) {
+    return nativeObjectRegistry.register(new NativeCanvas());
+  }
+
+  @Implementation(minSdk = O)
+  protected static int nGetSaveCount(long canvasHandle) {
+    return nativeObjectRegistry.getNativeObject(canvasHandle).getSaveCount();
+  }
+
+  @Implementation(minSdk = O)
+  protected static int nSave(long canvasHandle, int saveFlags) {
+    return nativeObjectRegistry.getNativeObject(canvasHandle).save();
+  }
+
+  @Implementation(maxSdk = KITKAT_WATCH)
+  protected static int native_saveLayer(int nativeCanvas, RectF bounds, int paint, int layerFlags) {
+    return nativeObjectRegistry.getNativeObject(nativeCanvas).save();
+  }
+
+  @Implementation(maxSdk = KITKAT_WATCH)
+  protected static int native_saveLayer(
+      int nativeCanvas, float l, float t, float r, float b, int paint, int layerFlags) {
+    return nativeObjectRegistry.getNativeObject(nativeCanvas).save();
+  }
+
+  @Implementation(minSdk = LOLLIPOP, maxSdk = N_MR1)
+  protected static int native_saveLayer(
+      long nativeCanvas, float l, float t, float r, float b, long nativePaint, int layerFlags) {
+    return nativeObjectRegistry.getNativeObject(nativeCanvas).save();
+  }
+
+  @Implementation(minSdk = O, maxSdk = R)
+  protected static int nSaveLayer(
+      long nativeCanvas, float l, float t, float r, float b, long nativePaint, int layerFlags) {
+    return nativeObjectRegistry.getNativeObject(nativeCanvas).save();
+  }
+
+  @Implementation(minSdk = S)
+  protected static int nSaveLayer(
+      long nativeCanvas, float l, float t, float r, float b, long nativePaint) {
+    return nativeObjectRegistry.getNativeObject(nativeCanvas).save();
+  }
+
+  @Implementation(maxSdk = KITKAT_WATCH)
+  protected static int native_saveLayerAlpha(
+      int nativeCanvas, RectF bounds, int alpha, int layerFlags) {
+    return nativeObjectRegistry.getNativeObject(nativeCanvas).save();
+  }
+
+  @Implementation(maxSdk = KITKAT_WATCH)
+  protected static int native_saveLayerAlpha(
+      int nativeCanvas, float l, float t, float r, float b, int alpha, int layerFlags) {
+    return nativeObjectRegistry.getNativeObject(nativeCanvas).save();
+  }
+
+  @Implementation(minSdk = LOLLIPOP, maxSdk = N_MR1)
+  protected static int native_saveLayerAlpha(
+      long nativeCanvas, float l, float t, float r, float b, int alpha, int layerFlags) {
+    return nativeObjectRegistry.getNativeObject(nativeCanvas).save();
+  }
+
+  @Implementation(minSdk = O, maxSdk = R)
+  protected static int nSaveLayerAlpha(
+      long nativeCanvas, float l, float t, float r, float b, int alpha, int layerFlags) {
+    return nativeObjectRegistry.getNativeObject(nativeCanvas).save();
+  }
+
+  @Implementation(minSdk = S)
+  protected static int nSaveLayerAlpha(
+      long nativeCanvas, float l, float t, float r, float b, int alpha) {
+    return nativeObjectRegistry.getNativeObject(nativeCanvas).save();
+  }
+
+  @Implementation(minSdk = O)
+  protected static boolean nRestore(long canvasHandle) {
+    return nativeObjectRegistry.getNativeObject(canvasHandle).restore();
+  }
+
+  @Implementation(minSdk = O)
+  protected static void nRestoreToCount(long canvasHandle, int saveCount) {
+    nativeObjectRegistry.getNativeObject(canvasHandle).restoreToCount(saveCount);
+  }
+
+  private static class PathPaintHistoryEvent {
+    private final Path drawnPath;
+    private final Paint pathPaint;
+
+    PathPaintHistoryEvent(Path drawnPath, Paint pathPaint) {
+      this.drawnPath = drawnPath;
+      this.pathPaint = pathPaint;
+    }
+  }
+
+  @Resetter
+  public static void reset() {
+    nativeObjectRegistry.clear();
+  }
+
+  @SuppressWarnings("MemberName")
+  @ForType(Canvas.class)
+  private interface CanvasReflector {
+    @Direct
+    void __constructor__(Bitmap bitmap);
+
+    @Direct
+    void release();
+  }
+
+  private static class NativeCanvas {
+    private int saveCount = 1;
+
+    int save() {
+      return saveCount++;
+    }
+
+    boolean restore() {
+      if (saveCount > 1) {
+        saveCount--;
+        return true;
+      } else {
+        return false;
+      }
+    }
+
+    int getSaveCount() {
+      return saveCount;
+    }
+
+    void restoreToCount(int saveCount) {
+      if (saveCount > 0) {
+        this.saveCount = saveCount;
+      }
+    }
+  }
+}
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowLegacyMatrix.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowLegacyMatrix.java
new file mode 100644
index 0000000..a85af0c
--- /dev/null
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowLegacyMatrix.java
@@ -0,0 +1,662 @@
+package org.robolectric.shadows;
+
+import static android.os.Build.VERSION_CODES.KITKAT;
+import static android.os.Build.VERSION_CODES.LOLLIPOP;
+
+import android.graphics.Matrix;
+import android.graphics.Matrix.ScaleToFit;
+import android.graphics.PointF;
+import android.graphics.RectF;
+import java.awt.geom.AffineTransform;
+import java.util.ArrayDeque;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Deque;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import org.robolectric.annotation.Implementation;
+import org.robolectric.annotation.Implements;
+import org.robolectric.shadow.api.Shadow;
+
+@SuppressWarnings({"UnusedDeclaration"})
+@Implements(value = Matrix.class, isInAndroidSdk = false)
+public class ShadowLegacyMatrix extends ShadowMatrix {
+
+  private static final float EPSILON = 1e-3f;
+
+  private final Deque<String> preOps = new ArrayDeque<>();
+  private final Deque<String> postOps = new ArrayDeque<>();
+  private final Map<String, String> setOps = new LinkedHashMap<>();
+
+  private SimpleMatrix simpleMatrix = SimpleMatrix.newIdentityMatrix();
+
+  @Implementation
+  protected void __constructor__(Matrix src) {
+    set(src);
+  }
+
+  /**
+   * A list of all 'pre' operations performed on this Matrix. The last operation performed will be
+   * first in the list.
+   *
+   * @return A list of all 'pre' operations performed on this Matrix.
+   */
+  @Override
+  public List<String> getPreOperations() {
+    return Collections.unmodifiableList(new ArrayList<>(preOps));
+  }
+
+  /**
+   * A list of all 'post' operations performed on this Matrix. The last operation performed will be
+   * last in the list.
+   *
+   * @return A list of all 'post' operations performed on this Matrix.
+   */
+  @Override
+  public List<String> getPostOperations() {
+    return Collections.unmodifiableList(new ArrayList<>(postOps));
+  }
+
+  /**
+   * A map of all 'set' operations performed on this Matrix.
+   *
+   * @return A map of all 'set' operations performed on this Matrix.
+   */
+  @Override
+  public Map<String, String> getSetOperations() {
+    return Collections.unmodifiableMap(new LinkedHashMap<>(setOps));
+  }
+
+  @Implementation
+  protected boolean isIdentity() {
+    return simpleMatrix.equals(SimpleMatrix.IDENTITY);
+  }
+
+  @Implementation(minSdk = LOLLIPOP)
+  protected boolean isAffine() {
+    return simpleMatrix.isAffine();
+  }
+
+  @Implementation
+  protected boolean rectStaysRect() {
+    return simpleMatrix.rectStaysRect();
+  }
+
+  @Implementation
+  protected void getValues(float[] values) {
+    simpleMatrix.getValues(values);
+  }
+
+  @Implementation
+  protected void setValues(float[] values) {
+    simpleMatrix = new SimpleMatrix(values);
+  }
+
+  @Implementation
+  protected void set(Matrix src) {
+    reset();
+    if (src != null) {
+      ShadowLegacyMatrix shadowMatrix = Shadow.extract(src);
+      preOps.addAll(shadowMatrix.preOps);
+      postOps.addAll(shadowMatrix.postOps);
+      setOps.putAll(shadowMatrix.setOps);
+      simpleMatrix = new SimpleMatrix(getSimpleMatrix(src));
+    }
+  }
+
+  @Implementation
+  protected void reset() {
+    preOps.clear();
+    postOps.clear();
+    setOps.clear();
+    simpleMatrix = SimpleMatrix.newIdentityMatrix();
+  }
+
+  @Implementation
+  protected void setTranslate(float dx, float dy) {
+    setOps.put(TRANSLATE, dx + " " + dy);
+    simpleMatrix = SimpleMatrix.translate(dx, dy);
+  }
+
+  @Implementation
+  protected void setScale(float sx, float sy, float px, float py) {
+    setOps.put(SCALE, sx + " " + sy + " " + px + " " + py);
+    simpleMatrix = SimpleMatrix.scale(sx, sy, px, py);
+  }
+
+  @Implementation
+  protected void setScale(float sx, float sy) {
+    setOps.put(SCALE, sx + " " + sy);
+    simpleMatrix = SimpleMatrix.scale(sx, sy);
+  }
+
+  @Implementation
+  protected void setRotate(float degrees, float px, float py) {
+    setOps.put(ROTATE, degrees + " " + px + " " + py);
+    simpleMatrix = SimpleMatrix.rotate(degrees, px, py);
+  }
+
+  @Implementation
+  protected void setRotate(float degrees) {
+    setOps.put(ROTATE, Float.toString(degrees));
+    simpleMatrix = SimpleMatrix.rotate(degrees);
+  }
+
+  @Implementation
+  protected void setSinCos(float sinValue, float cosValue, float px, float py) {
+    setOps.put(SINCOS, sinValue + " " + cosValue + " " + px + " " + py);
+    simpleMatrix = SimpleMatrix.sinCos(sinValue, cosValue, px, py);
+  }
+
+  @Implementation
+  protected void setSinCos(float sinValue, float cosValue) {
+    setOps.put(SINCOS, sinValue + " " + cosValue);
+    simpleMatrix = SimpleMatrix.sinCos(sinValue, cosValue);
+  }
+
+  @Implementation
+  protected void setSkew(float kx, float ky, float px, float py) {
+    setOps.put(SKEW, kx + " " + ky + " " + px + " " + py);
+    simpleMatrix = SimpleMatrix.skew(kx, ky, px, py);
+  }
+
+  @Implementation
+  protected void setSkew(float kx, float ky) {
+    setOps.put(SKEW, kx + " " + ky);
+    simpleMatrix = SimpleMatrix.skew(kx, ky);
+  }
+
+  @Implementation
+  protected boolean setConcat(Matrix a, Matrix b) {
+    simpleMatrix = getSimpleMatrix(a).multiply(getSimpleMatrix(b));
+    return true;
+  }
+
+  @Implementation
+  protected boolean preTranslate(float dx, float dy) {
+    preOps.addFirst(TRANSLATE + " " + dx + " " + dy);
+    return preConcat(SimpleMatrix.translate(dx, dy));
+  }
+
+  @Implementation
+  protected boolean preScale(float sx, float sy, float px, float py) {
+    preOps.addFirst(SCALE + " " + sx + " " + sy + " " + px + " " + py);
+    return preConcat(SimpleMatrix.scale(sx, sy, px, py));
+  }
+
+  @Implementation
+  protected boolean preScale(float sx, float sy) {
+    preOps.addFirst(SCALE + " " + sx + " " + sy);
+    return preConcat(SimpleMatrix.scale(sx, sy));
+  }
+
+  @Implementation
+  protected boolean preRotate(float degrees, float px, float py) {
+    preOps.addFirst(ROTATE + " " + degrees + " " + px + " " + py);
+    return preConcat(SimpleMatrix.rotate(degrees, px, py));
+  }
+
+  @Implementation
+  protected boolean preRotate(float degrees) {
+    preOps.addFirst(ROTATE + " " + Float.toString(degrees));
+    return preConcat(SimpleMatrix.rotate(degrees));
+  }
+
+  @Implementation
+  protected boolean preSkew(float kx, float ky, float px, float py) {
+    preOps.addFirst(SKEW + " " + kx + " " + ky + " " + px + " " + py);
+    return preConcat(SimpleMatrix.skew(kx, ky, px, py));
+  }
+
+  @Implementation
+  protected boolean preSkew(float kx, float ky) {
+    preOps.addFirst(SKEW + " " + kx + " " + ky);
+    return preConcat(SimpleMatrix.skew(kx, ky));
+  }
+
+  @Implementation
+  protected boolean preConcat(Matrix other) {
+    preOps.addFirst(MATRIX + " " + other);
+    return preConcat(getSimpleMatrix(other));
+  }
+
+  @Implementation
+  protected boolean postTranslate(float dx, float dy) {
+    postOps.addLast(TRANSLATE + " " + dx + " " + dy);
+    return postConcat(SimpleMatrix.translate(dx, dy));
+  }
+
+  @Implementation
+  protected boolean postScale(float sx, float sy, float px, float py) {
+    postOps.addLast(SCALE + " " + sx + " " + sy + " " + px + " " + py);
+    return postConcat(SimpleMatrix.scale(sx, sy, px, py));
+  }
+
+  @Implementation
+  protected boolean postScale(float sx, float sy) {
+    postOps.addLast(SCALE + " " + sx + " " + sy);
+    return postConcat(SimpleMatrix.scale(sx, sy));
+  }
+
+  @Implementation
+  protected boolean postRotate(float degrees, float px, float py) {
+    postOps.addLast(ROTATE + " " + degrees + " " + px + " " + py);
+    return postConcat(SimpleMatrix.rotate(degrees, px, py));
+  }
+
+  @Implementation
+  protected boolean postRotate(float degrees) {
+    postOps.addLast(ROTATE + " " + Float.toString(degrees));
+    return postConcat(SimpleMatrix.rotate(degrees));
+  }
+
+  @Implementation
+  protected boolean postSkew(float kx, float ky, float px, float py) {
+    postOps.addLast(SKEW + " " + kx + " " + ky + " " + px + " " + py);
+    return postConcat(SimpleMatrix.skew(kx, ky, px, py));
+  }
+
+  @Implementation
+  protected boolean postSkew(float kx, float ky) {
+    postOps.addLast(SKEW + " " + kx + " " + ky);
+    return postConcat(SimpleMatrix.skew(kx, ky));
+  }
+
+  @Implementation
+  protected boolean postConcat(Matrix other) {
+    postOps.addLast(MATRIX + " " + other);
+    return postConcat(getSimpleMatrix(other));
+  }
+
+  @Implementation
+  protected boolean invert(Matrix inverse) {
+    final SimpleMatrix inverseMatrix = simpleMatrix.invert();
+    if (inverseMatrix != null) {
+      if (inverse != null) {
+        final ShadowLegacyMatrix shadowInverse = Shadow.extract(inverse);
+        shadowInverse.simpleMatrix = inverseMatrix;
+      }
+      return true;
+    }
+    return false;
+  }
+
+  boolean hasPerspective() {
+    return (simpleMatrix.mValues[6] != 0 || simpleMatrix.mValues[7] != 0 || simpleMatrix.mValues[8] != 1);
+  }
+
+  protected AffineTransform getAffineTransform() {
+    // the AffineTransform constructor takes the value in a different order
+    // for a matrix [ 0 1 2 ]
+    //              [ 3 4 5 ]
+    // the order is 0, 3, 1, 4, 2, 5...
+    return new AffineTransform(
+        simpleMatrix.mValues[0],
+        simpleMatrix.mValues[3],
+        simpleMatrix.mValues[1],
+        simpleMatrix.mValues[4],
+        simpleMatrix.mValues[2],
+        simpleMatrix.mValues[5]);
+  }
+
+  public PointF mapPoint(float x, float y) {
+    return simpleMatrix.transform(new PointF(x, y));
+  }
+
+  public PointF mapPoint(PointF point) {
+    return simpleMatrix.transform(point);
+  }
+
+  @Implementation
+  protected boolean mapRect(RectF destination, RectF source) {
+    final PointF leftTop = mapPoint(source.left, source.top);
+    final PointF rightBottom = mapPoint(source.right, source.bottom);
+    destination.set(
+        Math.min(leftTop.x, rightBottom.x),
+        Math.min(leftTop.y, rightBottom.y),
+        Math.max(leftTop.x, rightBottom.x),
+        Math.max(leftTop.y, rightBottom.y));
+    return true;
+  }
+
+  @Implementation
+  protected void mapPoints(float[] dst, int dstIndex, float[] src, int srcIndex, int pointCount) {
+    for (int i = 0; i < pointCount; i++) {
+      final PointF mapped = mapPoint(src[srcIndex + i * 2], src[srcIndex + i * 2 + 1]);
+      dst[dstIndex + i * 2] = mapped.x;
+      dst[dstIndex + i * 2 + 1] = mapped.y;
+    }
+  }
+
+  @Implementation
+  protected void mapVectors(float[] dst, int dstIndex, float[] src, int srcIndex, int vectorCount) {
+    final float transX = simpleMatrix.mValues[Matrix.MTRANS_X];
+    final float transY = simpleMatrix.mValues[Matrix.MTRANS_Y];
+
+    simpleMatrix.mValues[Matrix.MTRANS_X] = 0;
+    simpleMatrix.mValues[Matrix.MTRANS_Y] = 0;
+
+    for (int i = 0; i < vectorCount; i++) {
+      final PointF mapped = mapPoint(src[srcIndex + i * 2], src[srcIndex + i * 2 + 1]);
+      dst[dstIndex + i * 2] = mapped.x;
+      dst[dstIndex + i * 2 + 1] = mapped.y;
+    }
+
+    simpleMatrix.mValues[Matrix.MTRANS_X] = transX;
+    simpleMatrix.mValues[Matrix.MTRANS_Y] = transY;
+  }
+
+  @Implementation
+  protected float mapRadius(float radius) {
+    float[] src = new float[] {radius, 0.f, 0.f, radius};
+    mapVectors(src, 0, src, 0, 2);
+
+    float l1 = (float) Math.hypot(src[0], src[1]);
+    float l2 = (float) Math.hypot(src[2], src[3]);
+    return (float) Math.sqrt(l1 * l2);
+  }
+
+  @Implementation
+  protected boolean setRectToRect(RectF src, RectF dst, Matrix.ScaleToFit stf) {
+    if (src.isEmpty()) {
+      reset();
+      return false;
+    }
+    return simpleMatrix.setRectToRect(src, dst, stf);
+  }
+
+  @Implementation
+  @Override
+  public boolean equals(Object obj) {
+    if (obj instanceof Matrix) {
+        return getSimpleMatrix(((Matrix) obj)).equals(simpleMatrix);
+    } else {
+        return obj instanceof ShadowMatrix && obj.equals(simpleMatrix);
+    }
+  }
+
+  @Implementation(minSdk = KITKAT)
+  @Override
+  public int hashCode() {
+      return Objects.hashCode(simpleMatrix);
+  }
+
+  @Override
+  public String getDescription() {
+    return "Matrix[pre=" + preOps + ", set=" + setOps + ", post=" + postOps + "]";
+  }
+
+  private static SimpleMatrix getSimpleMatrix(Matrix matrix) {
+    final ShadowLegacyMatrix otherMatrix = Shadow.extract(matrix);
+    return otherMatrix.simpleMatrix;
+  }
+
+  private boolean postConcat(SimpleMatrix matrix) {
+    simpleMatrix = matrix.multiply(simpleMatrix);
+    return true;
+  }
+
+  private boolean preConcat(SimpleMatrix matrix) {
+    simpleMatrix = simpleMatrix.multiply(matrix);
+    return true;
+  }
+
+  /**
+   * A simple implementation of an immutable matrix.
+   */
+  private static class SimpleMatrix {
+    private static final SimpleMatrix IDENTITY = newIdentityMatrix();
+
+    private static SimpleMatrix newIdentityMatrix() {
+      return new SimpleMatrix(
+          new float[] {
+            1.0f, 0.0f, 0.0f,
+            0.0f, 1.0f, 0.0f,
+            0.0f, 0.0f, 1.0f,
+          });
+    }
+
+    private final float[] mValues;
+
+    SimpleMatrix(SimpleMatrix matrix) {
+      mValues = Arrays.copyOf(matrix.mValues, matrix.mValues.length);
+    }
+
+    private SimpleMatrix(float[] values) {
+      if (values.length != 9) {
+        throw new ArrayIndexOutOfBoundsException();
+      }
+      mValues = Arrays.copyOf(values, 9);
+    }
+
+    public boolean isAffine() {
+      return mValues[6] == 0.0f && mValues[7] == 0.0f && mValues[8] == 1.0f;
+    }
+
+    public boolean rectStaysRect() {
+      final float m00 = mValues[0];
+      final float m01 = mValues[1];
+      final float m10 = mValues[3];
+      final float m11 = mValues[4];
+      return (m00 == 0 && m11 == 0 && m01 != 0 && m10 != 0) || (m00 != 0 && m11 != 0 && m01 == 0 && m10 == 0);
+    }
+
+    public void getValues(float[] values) {
+      if (values.length < 9) {
+        throw new ArrayIndexOutOfBoundsException();
+      }
+      System.arraycopy(mValues, 0, values, 0, 9);
+    }
+
+    public static SimpleMatrix translate(float dx, float dy) {
+      return new SimpleMatrix(new float[] {
+          1.0f, 0.0f, dx,
+          0.0f, 1.0f, dy,
+          0.0f, 0.0f, 1.0f,
+      });
+    }
+
+    public static SimpleMatrix scale(float sx, float sy, float px, float py) {
+      return new SimpleMatrix(new float[] {
+          sx,   0.0f, px * (1 - sx),
+          0.0f, sy,   py * (1 - sy),
+          0.0f, 0.0f, 1.0f,
+      });
+    }
+
+    public static SimpleMatrix scale(float sx, float sy) {
+      return new SimpleMatrix(new float[] {
+          sx,   0.0f, 0.0f,
+          0.0f, sy,   0.0f,
+          0.0f, 0.0f, 1.0f,
+      });
+    }
+
+    public static SimpleMatrix rotate(float degrees, float px, float py) {
+      final double radians = Math.toRadians(degrees);
+      final float sin = (float) Math.sin(radians);
+      final float cos = (float) Math.cos(radians);
+      return sinCos(sin, cos, px, py);
+    }
+
+    public static SimpleMatrix rotate(float degrees) {
+      final double radians = Math.toRadians(degrees);
+      final float sin = (float) Math.sin(radians);
+      final float cos = (float) Math.cos(radians);
+      return sinCos(sin, cos);
+    }
+
+    public static SimpleMatrix sinCos(float sin, float cos, float px, float py) {
+      return new SimpleMatrix(new float[] {
+          cos,  -sin, sin * py + (1 - cos) * px,
+          sin,  cos,  -sin * px + (1 - cos) * py,
+          0.0f, 0.0f, 1.0f,
+      });
+    }
+
+    public static SimpleMatrix sinCos(float sin, float cos) {
+      return new SimpleMatrix(new float[] {
+          cos,  -sin, 0.0f,
+          sin,  cos,  0.0f,
+          0.0f, 0.0f, 1.0f,
+      });
+    }
+
+    public static SimpleMatrix skew(float kx, float ky, float px, float py) {
+      return new SimpleMatrix(new float[] {
+          1.0f, kx,   -kx * py,
+          ky,   1.0f, -ky * px,
+          0.0f, 0.0f, 1.0f,
+      });
+    }
+
+    public static SimpleMatrix skew(float kx, float ky) {
+      return new SimpleMatrix(new float[] {
+          1.0f, kx,   0.0f,
+          ky,   1.0f, 0.0f,
+          0.0f, 0.0f, 1.0f,
+      });
+    }
+
+    public SimpleMatrix multiply(SimpleMatrix matrix) {
+      final float[] values = new float[9];
+      for (int i = 0; i < values.length; ++i) {
+        final int row = i / 3;
+        final int col = i % 3;
+        for (int j = 0; j < 3; ++j) {
+          values[i] += mValues[row * 3 + j] * matrix.mValues[j * 3 + col];
+        }
+      }
+      return new SimpleMatrix(values);
+    }
+
+    public SimpleMatrix invert() {
+      final float invDet = inverseDeterminant();
+      if (invDet == 0) {
+        return null;
+      }
+
+      final float[] src = mValues;
+      final float[] dst = new float[9];
+      dst[0] = cross_scale(src[4], src[8], src[5], src[7], invDet);
+      dst[1] = cross_scale(src[2], src[7], src[1], src[8], invDet);
+      dst[2] = cross_scale(src[1], src[5], src[2], src[4], invDet);
+
+      dst[3] = cross_scale(src[5], src[6], src[3], src[8], invDet);
+      dst[4] = cross_scale(src[0], src[8], src[2], src[6], invDet);
+      dst[5] = cross_scale(src[2], src[3], src[0], src[5], invDet);
+
+      dst[6] = cross_scale(src[3], src[7], src[4], src[6], invDet);
+      dst[7] = cross_scale(src[1], src[6], src[0], src[7], invDet);
+      dst[8] = cross_scale(src[0], src[4], src[1], src[3], invDet);
+      return new SimpleMatrix(dst);
+    }
+
+    public PointF transform(PointF point) {
+      return new PointF(
+          point.x * mValues[0] + point.y * mValues[1] + mValues[2],
+          point.x * mValues[3] + point.y * mValues[4] + mValues[5]);
+    }
+
+    // See: https://android.googlesource.com/platform/frameworks/base/+/6fca81de9b2079ec88e785f58bf49bf1f0c105e2/tools/layoutlib/bridge/src/android/graphics/Matrix_Delegate.java
+    protected boolean setRectToRect(RectF src, RectF dst, ScaleToFit stf) {
+      if (dst.isEmpty()) {
+        mValues[0] =
+            mValues[1] =
+                mValues[2] = mValues[3] = mValues[4] = mValues[5] = mValues[6] = mValues[7] = 0;
+        mValues[8] = 1;
+      } else {
+        float tx = dst.width() / src.width();
+        float sx = dst.width() / src.width();
+        float ty = dst.height() / src.height();
+        float sy = dst.height() / src.height();
+        boolean xLarger = false;
+
+        if (stf != ScaleToFit.FILL) {
+          if (sx > sy) {
+            xLarger = true;
+            sx = sy;
+          } else {
+            sy = sx;
+          }
+        }
+
+        tx = dst.left - src.left * sx;
+        ty = dst.top - src.top * sy;
+        if (stf == ScaleToFit.CENTER || stf == ScaleToFit.END) {
+          float diff;
+
+          if (xLarger) {
+            diff = dst.width() - src.width() * sy;
+          } else {
+            diff = dst.height() - src.height() * sy;
+          }
+
+          if (stf == ScaleToFit.CENTER) {
+            diff = diff / 2;
+          }
+
+          if (xLarger) {
+            tx += diff;
+          } else {
+            ty += diff;
+          }
+        }
+
+        mValues[0] = sx;
+        mValues[4] = sy;
+        mValues[2] = tx;
+        mValues[5] = ty;
+        mValues[1] = mValues[3] = mValues[6] = mValues[7] = 0;
+      }
+      // shared cleanup
+      mValues[8] = 1;
+      return true;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+      return this == o || (o instanceof SimpleMatrix && equals((SimpleMatrix) o));
+    }
+
+    @SuppressWarnings("NonOverridingEquals")
+    public boolean equals(SimpleMatrix matrix) {
+      if (matrix == null) {
+        return false;
+      }
+      for (int i = 0; i < mValues.length; i++) {
+        if (!isNearlyZero(matrix.mValues[i] - mValues[i])) {
+          return false;
+        }
+      }
+      return true;
+    }
+
+    @Override
+    public int hashCode() {
+      return Arrays.hashCode(mValues);
+    }
+
+    private static boolean isNearlyZero(float value) {
+      return Math.abs(value) < EPSILON;
+    }
+
+    private static float cross(float a, float b, float c, float d) {
+      return a * b - c * d;
+    }
+
+    private static float cross_scale(float a, float b, float c, float d, float scale) {
+      return cross(a, b, c, d) * scale;
+    }
+
+    private float inverseDeterminant() {
+      final float determinant = mValues[0] * cross(mValues[4], mValues[8], mValues[5], mValues[7]) +
+          mValues[1] * cross(mValues[5], mValues[6], mValues[3], mValues[8]) +
+          mValues[2] * cross(mValues[3], mValues[7], mValues[4], mValues[6]);
+      return isNearlyZero(determinant) ? 0.0f : 1.0f / determinant;
+    }
+  }
+}
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowLegacyPath.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowLegacyPath.java
new file mode 100644
index 0000000..b4f113a
--- /dev/null
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowLegacyPath.java
@@ -0,0 +1,558 @@
+package org.robolectric.shadows;
+
+import static android.os.Build.VERSION_CODES.JELLY_BEAN;
+import static android.os.Build.VERSION_CODES.KITKAT;
+import static android.os.Build.VERSION_CODES.LOLLIPOP;
+import static org.robolectric.shadow.api.Shadow.extract;
+import static org.robolectric.shadows.ShadowPath.Point.Type.LINE_TO;
+import static org.robolectric.shadows.ShadowPath.Point.Type.MOVE_TO;
+
+import android.graphics.Matrix;
+import android.graphics.Path;
+import android.graphics.Path.Direction;
+import android.graphics.RectF;
+import android.util.Log;
+import java.awt.geom.AffineTransform;
+import java.awt.geom.Arc2D;
+import java.awt.geom.Area;
+import java.awt.geom.Ellipse2D;
+import java.awt.geom.GeneralPath;
+import java.awt.geom.Path2D;
+import java.awt.geom.PathIterator;
+import java.awt.geom.Point2D;
+import java.awt.geom.Rectangle2D;
+import java.awt.geom.RoundRectangle2D;
+import java.util.ArrayList;
+import java.util.List;
+import org.robolectric.annotation.Implementation;
+import org.robolectric.annotation.Implements;
+import org.robolectric.annotation.RealObject;
+
+/** The shadow only supports straight-line paths. */
+@SuppressWarnings({"UnusedDeclaration"})
+@Implements(value = Path.class, isInAndroidSdk = false)
+public class ShadowLegacyPath extends ShadowPath {
+  private static final String TAG = ShadowLegacyPath.class.getSimpleName();
+  private static final float EPSILON = 1e-4f;
+
+  @RealObject private Path realObject;
+
+  private List<Point> points = new ArrayList<>();
+
+  private float mLastX = 0;
+  private float mLastY = 0;
+  private Path2D mPath = new Path2D.Double();
+  private boolean mCachedIsEmpty = true;
+  private Path.FillType mFillType = Path.FillType.WINDING;
+  protected boolean isSimplePath;
+
+  @Implementation
+  protected void __constructor__(Path path) {
+    ShadowLegacyPath shadowPath = extract(path);
+    points = new ArrayList<>(shadowPath.getPoints());
+    mPath.append(shadowPath.mPath, /*connect=*/ false);
+    mFillType = shadowPath.getFillType();
+  }
+
+  Path2D getJavaShape() {
+    return mPath;
+  }
+
+  @Implementation
+  protected void moveTo(float x, float y) {
+    mPath.moveTo(mLastX = x, mLastY = y);
+
+    // Legacy recording behavior
+    Point p = new Point(x, y, MOVE_TO);
+    points.add(p);
+  }
+
+  @Implementation
+  protected void lineTo(float x, float y) {
+    if (!hasPoints()) {
+      mPath.moveTo(mLastX = 0, mLastY = 0);
+    }
+    mPath.lineTo(mLastX = x, mLastY = y);
+
+    // Legacy recording behavior
+    Point point = new Point(x, y, LINE_TO);
+    points.add(point);
+  }
+
+  @Implementation
+  protected void quadTo(float x1, float y1, float x2, float y2) {
+    isSimplePath = false;
+    if (!hasPoints()) {
+      moveTo(0, 0);
+    }
+    mPath.quadTo(x1, y1, mLastX = x2, mLastY = y2);
+  }
+
+  @Implementation
+  protected void cubicTo(float x1, float y1, float x2, float y2, float x3, float y3) {
+    if (!hasPoints()) {
+      mPath.moveTo(0, 0);
+    }
+    mPath.curveTo(x1, y1, x2, y2, mLastX = x3, mLastY = y3);
+  }
+
+  private boolean hasPoints() {
+    return !mPath.getPathIterator(null).isDone();
+  }
+
+  @Implementation
+  protected void reset() {
+    mPath.reset();
+    mLastX = 0;
+    mLastY = 0;
+
+    // Legacy recording behavior
+    points.clear();
+  }
+
+  @Implementation(minSdk = LOLLIPOP)
+  protected float[] approximate(float acceptableError) {
+    PathIterator iterator = mPath.getPathIterator(null, acceptableError);
+
+    float segment[] = new float[6];
+    float totalLength = 0;
+    ArrayList<Point2D.Float> points = new ArrayList<Point2D.Float>();
+    Point2D.Float previousPoint = null;
+    while (!iterator.isDone()) {
+      int type = iterator.currentSegment(segment);
+      Point2D.Float currentPoint = new Point2D.Float(segment[0], segment[1]);
+      // MoveTo shouldn't affect the length
+      if (previousPoint != null && type != PathIterator.SEG_MOVETO) {
+        totalLength += (float) currentPoint.distance(previousPoint);
+      }
+      previousPoint = currentPoint;
+      points.add(currentPoint);
+      iterator.next();
+    }
+
+    int nPoints = points.size();
+    float[] result = new float[nPoints * 3];
+    previousPoint = null;
+    // Distance that we've covered so far. Used to calculate the fraction of the path that
+    // we've covered up to this point.
+    float walkedDistance = .0f;
+    for (int i = 0; i < nPoints; i++) {
+      Point2D.Float point = points.get(i);
+      float distance = previousPoint != null ? (float) previousPoint.distance(point) : .0f;
+      walkedDistance += distance;
+      result[i * 3] = walkedDistance / totalLength;
+      result[i * 3 + 1] = point.x;
+      result[i * 3 + 2] = point.y;
+
+      previousPoint = point;
+    }
+
+    return result;
+  }
+
+  /**
+   * @return all the points that have been added to the {@code Path}
+   */
+  @Override
+  public List<Point> getPoints() {
+    return points;
+  }
+
+  @Implementation
+  protected void rewind() {
+    // call out to reset since there's nothing to optimize in
+    // terms of data structs.
+    reset();
+  }
+
+  @Implementation
+  protected void set(Path src) {
+    mPath.reset();
+
+    ShadowLegacyPath shadowSrc = extract(src);
+    setFillType(shadowSrc.mFillType);
+    mPath.append(shadowSrc.mPath, false /*connect*/);
+  }
+
+  @Implementation(minSdk = KITKAT)
+  protected boolean op(Path path1, Path path2, Path.Op op) {
+    Log.w(TAG, "android.graphics.Path#op() not supported yet.");
+    return false;
+  }
+
+  @Implementation(minSdk = LOLLIPOP)
+  protected boolean isConvex() {
+    Log.w(TAG, "android.graphics.Path#isConvex() not supported yet.");
+    return true;
+  }
+
+  @Implementation
+  protected Path.FillType getFillType() {
+    return mFillType;
+  }
+
+  @Implementation
+  protected void setFillType(Path.FillType fillType) {
+    mFillType = fillType;
+    mPath.setWindingRule(getWindingRule(fillType));
+  }
+
+  /**
+   * Returns the Java2D winding rules matching a given Android {@link
+   * android.graphics.Path.FillType}.
+   *
+   * @param type the android fill type
+   * @return the matching java2d winding rule.
+   */
+  private static int getWindingRule(Path.FillType type) {
+    switch (type) {
+      case WINDING:
+      case INVERSE_WINDING:
+        return GeneralPath.WIND_NON_ZERO;
+      case EVEN_ODD:
+      case INVERSE_EVEN_ODD:
+        return GeneralPath.WIND_EVEN_ODD;
+
+      default:
+        assert false;
+        return GeneralPath.WIND_NON_ZERO;
+    }
+  }
+
+  @Implementation
+  protected boolean isInverseFillType() {
+    throw new UnsupportedOperationException("isInverseFillType");
+  }
+
+  @Implementation
+  protected void toggleInverseFillType() {
+    throw new UnsupportedOperationException("toggleInverseFillType");
+  }
+
+  @Implementation
+  protected boolean isEmpty() {
+    if (!mCachedIsEmpty) {
+      return false;
+    }
+
+    mCachedIsEmpty = Boolean.TRUE;
+    for (PathIterator it = mPath.getPathIterator(null); !it.isDone(); it.next()) {
+      // int type = it.currentSegment(coords);
+      // if (type != PathIterator.SEG_MOVETO) {
+      // Once we know that the path is not empty, we do not need to check again unless
+      // Path#reset is called.
+      mCachedIsEmpty = false;
+      return false;
+      // }
+    }
+
+    return true;
+  }
+
+  @Implementation
+  protected boolean isRect(RectF rect) {
+    // create an Area that can test if the path is a rect
+    Area area = new Area(mPath);
+    if (area.isRectangular()) {
+      if (rect != null) {
+        fillBounds(rect);
+      }
+
+      return true;
+    }
+
+    return false;
+  }
+
+  @Implementation
+  protected void computeBounds(RectF bounds, boolean exact) {
+    fillBounds(bounds);
+  }
+
+  @Implementation
+  protected void incReserve(int extraPtCount) {
+    throw new UnsupportedOperationException("incReserve");
+  }
+
+  @Implementation
+  protected void rMoveTo(float dx, float dy) {
+    dx += mLastX;
+    dy += mLastY;
+    mPath.moveTo(mLastX = dx, mLastY = dy);
+  }
+
+  @Implementation
+  protected void rLineTo(float dx, float dy) {
+    if (!hasPoints()) {
+      mPath.moveTo(mLastX = 0, mLastY = 0);
+    }
+
+    if (Math.abs(dx) < EPSILON && Math.abs(dy) < EPSILON) {
+      // The delta is so small that this shouldn't generate a line
+      return;
+    }
+
+    dx += mLastX;
+    dy += mLastY;
+    mPath.lineTo(mLastX = dx, mLastY = dy);
+  }
+
+  @Implementation
+  protected void rQuadTo(float dx1, float dy1, float dx2, float dy2) {
+    if (!hasPoints()) {
+      mPath.moveTo(mLastX = 0, mLastY = 0);
+    }
+    dx1 += mLastX;
+    dy1 += mLastY;
+    dx2 += mLastX;
+    dy2 += mLastY;
+    mPath.quadTo(dx1, dy1, mLastX = dx2, mLastY = dy2);
+  }
+
+  @Implementation
+  protected void rCubicTo(float x1, float y1, float x2, float y2, float x3, float y3) {
+    if (!hasPoints()) {
+      mPath.moveTo(mLastX = 0, mLastY = 0);
+    }
+    x1 += mLastX;
+    y1 += mLastY;
+    x2 += mLastX;
+    y2 += mLastY;
+    x3 += mLastX;
+    y3 += mLastY;
+    mPath.curveTo(x1, y1, x2, y2, mLastX = x3, mLastY = y3);
+  }
+
+  @Implementation
+  protected void arcTo(RectF oval, float startAngle, float sweepAngle) {
+    arcTo(oval.left, oval.top, oval.right, oval.bottom, startAngle, sweepAngle, false);
+  }
+
+  @Implementation
+  protected void arcTo(RectF oval, float startAngle, float sweepAngle, boolean forceMoveTo) {
+    arcTo(oval.left, oval.top, oval.right, oval.bottom, startAngle, sweepAngle, forceMoveTo);
+  }
+
+  @Implementation(minSdk = LOLLIPOP)
+  protected void arcTo(
+      float left,
+      float top,
+      float right,
+      float bottom,
+      float startAngle,
+      float sweepAngle,
+      boolean forceMoveTo) {
+    isSimplePath = false;
+    Arc2D arc =
+        new Arc2D.Float(
+            left, top, right - left, bottom - top, -startAngle, -sweepAngle, Arc2D.OPEN);
+    mPath.append(arc, true /*connect*/);
+    if (hasPoints()) {
+      resetLastPointFromPath();
+    }
+  }
+
+  @Implementation
+  protected void close() {
+    if (!hasPoints()) {
+      mPath.moveTo(mLastX = 0, mLastY = 0);
+    }
+    mPath.closePath();
+  }
+
+  @Implementation
+  protected void addRect(RectF rect, Direction dir) {
+    addRect(rect.left, rect.top, rect.right, rect.bottom, dir);
+  }
+
+  @Implementation
+  protected void addRect(float left, float top, float right, float bottom, Path.Direction dir) {
+    moveTo(left, top);
+
+    switch (dir) {
+      case CW:
+        lineTo(right, top);
+        lineTo(right, bottom);
+        lineTo(left, bottom);
+        break;
+      case CCW:
+        lineTo(left, bottom);
+        lineTo(right, bottom);
+        lineTo(right, top);
+        break;
+    }
+
+    close();
+
+    resetLastPointFromPath();
+  }
+
+  @Implementation(minSdk = LOLLIPOP)
+  protected void addOval(float left, float top, float right, float bottom, Path.Direction dir) {
+    mPath.append(new Ellipse2D.Float(left, top, right - left, bottom - top), false);
+  }
+
+  @Implementation
+  protected void addCircle(float x, float y, float radius, Path.Direction dir) {
+    mPath.append(new Ellipse2D.Float(x - radius, y - radius, radius * 2, radius * 2), false);
+  }
+
+  @Implementation(minSdk = LOLLIPOP)
+  protected void addArc(
+      float left, float top, float right, float bottom, float startAngle, float sweepAngle) {
+    mPath.append(
+        new Arc2D.Float(
+            left, top, right - left, bottom - top, -startAngle, -sweepAngle, Arc2D.OPEN),
+        false);
+  }
+
+  @Implementation(minSdk = JELLY_BEAN)
+  protected void addRoundRect(RectF rect, float rx, float ry, Direction dir) {
+    addRoundRect(rect.left, rect.top, rect.right, rect.bottom, rx, ry, dir);
+  }
+
+  @Implementation(minSdk = JELLY_BEAN)
+  protected void addRoundRect(RectF rect, float[] radii, Direction dir) {
+    if (rect == null) {
+      throw new NullPointerException("need rect parameter");
+    }
+    addRoundRect(rect.left, rect.top, rect.right, rect.bottom, radii, dir);
+  }
+
+  @Implementation(minSdk = LOLLIPOP)
+  protected void addRoundRect(
+      float left, float top, float right, float bottom, float rx, float ry, Path.Direction dir) {
+    mPath.append(
+        new RoundRectangle2D.Float(left, top, right - left, bottom - top, rx * 2, ry * 2), false);
+  }
+
+  @Implementation(minSdk = LOLLIPOP)
+  protected void addRoundRect(
+      float left, float top, float right, float bottom, float[] radii, Path.Direction dir) {
+    if (radii.length < 8) {
+      throw new ArrayIndexOutOfBoundsException("radii[] needs 8 values");
+    }
+    isSimplePath = false;
+
+    float[] cornerDimensions = new float[radii.length];
+    for (int i = 0; i < radii.length; i++) {
+      cornerDimensions[i] = 2 * radii[i];
+    }
+    mPath.append(
+        new RoundRectangle(left, top, right - left, bottom - top, cornerDimensions), false);
+  }
+
+  @Implementation
+  protected void addPath(Path src, float dx, float dy) {
+    isSimplePath = false;
+    ShadowLegacyPath.addPath(realObject, src, AffineTransform.getTranslateInstance(dx, dy));
+  }
+
+  @Implementation
+  protected void addPath(Path src) {
+    isSimplePath = false;
+    ShadowLegacyPath.addPath(realObject, src, null);
+  }
+
+  @Implementation
+  protected void addPath(Path src, Matrix matrix) {
+    if (matrix == null) {
+      return;
+    }
+    ShadowLegacyPath shadowSrc = extract(src);
+    if (!shadowSrc.isSimplePath) isSimplePath = false;
+
+    ShadowLegacyMatrix shadowMatrix = extract(matrix);
+    ShadowLegacyPath.addPath(realObject, src, shadowMatrix.getAffineTransform());
+  }
+
+  private static void addPath(Path destPath, Path srcPath, AffineTransform transform) {
+    if (destPath == null) {
+      return;
+    }
+
+    if (srcPath == null) {
+      return;
+    }
+
+    ShadowLegacyPath shadowDestPath = extract(destPath);
+    ShadowLegacyPath shadowSrcPath = extract(srcPath);
+    if (transform != null) {
+      shadowDestPath.mPath.append(shadowSrcPath.mPath.getPathIterator(transform), false);
+    } else {
+      shadowDestPath.mPath.append(shadowSrcPath.mPath, false);
+    }
+  }
+
+  @Implementation
+  protected void offset(float dx, float dy, Path dst) {
+    if (dst != null) {
+      dst.set(realObject);
+    } else {
+      dst = realObject;
+    }
+    dst.offset(dx, dy);
+  }
+
+  @Implementation
+  protected void offset(float dx, float dy) {
+    GeneralPath newPath = new GeneralPath();
+
+    PathIterator iterator = mPath.getPathIterator(new AffineTransform(0, 0, dx, 0, 0, dy));
+
+    newPath.append(iterator, false /*connect*/);
+    mPath = newPath;
+  }
+
+  @Implementation
+  protected void setLastPoint(float dx, float dy) {
+    mLastX = dx;
+    mLastY = dy;
+  }
+
+  @Implementation
+  protected void transform(Matrix matrix, Path dst) {
+    ShadowLegacyMatrix shadowMatrix = extract(matrix);
+
+    if (shadowMatrix.hasPerspective()) {
+      Log.w(TAG, "android.graphics.Path#transform() only supports affine transformations.");
+    }
+
+    GeneralPath newPath = new GeneralPath();
+
+    PathIterator iterator = mPath.getPathIterator(shadowMatrix.getAffineTransform());
+    newPath.append(iterator, false /*connect*/);
+
+    if (dst != null) {
+      ShadowLegacyPath shadowPath = extract(dst);
+      shadowPath.mPath = newPath;
+    } else {
+      mPath = newPath;
+    }
+  }
+
+  @Implementation
+  protected void transform(Matrix matrix) {
+    transform(matrix, null);
+  }
+
+  /**
+   * Fills the given {@link RectF} with the path bounds.
+   *
+   * @param bounds the RectF to be filled.
+   */
+  @Override
+  public void fillBounds(RectF bounds) {
+    Rectangle2D rect = mPath.getBounds2D();
+    bounds.left = (float) rect.getMinX();
+    bounds.right = (float) rect.getMaxX();
+    bounds.top = (float) rect.getMinY();
+    bounds.bottom = (float) rect.getMaxY();
+  }
+
+  private void resetLastPointFromPath() {
+    Point2D last = mPath.getCurrentPoint();
+    mLastX = (float) last.getX();
+    mLastY = (float) last.getY();
+  }
+}
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowLegacyTypeface.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowLegacyTypeface.java
new file mode 100644
index 0000000..378bbb0
--- /dev/null
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowLegacyTypeface.java
@@ -0,0 +1,273 @@
+package org.robolectric.shadows;
+
+import static android.os.Build.VERSION_CODES.KITKAT;
+import static android.os.Build.VERSION_CODES.LOLLIPOP;
+import static android.os.Build.VERSION_CODES.N_MR1;
+import static android.os.Build.VERSION_CODES.O;
+import static android.os.Build.VERSION_CODES.O_MR1;
+import static android.os.Build.VERSION_CODES.P;
+import static android.os.Build.VERSION_CODES.Q;
+import static android.os.Build.VERSION_CODES.R;
+import static android.os.Build.VERSION_CODES.S;
+import static org.robolectric.RuntimeEnvironment.getApiLevel;
+import static org.robolectric.Shadows.shadowOf;
+
+import android.annotation.SuppressLint;
+import android.content.res.AssetManager;
+import android.graphics.FontFamily;
+import android.graphics.Typeface;
+import android.util.ArrayMap;
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Objects;
+import java.util.concurrent.atomic.AtomicLong;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.annotation.HiddenApi;
+import org.robolectric.annotation.Implementation;
+import org.robolectric.annotation.Implements;
+import org.robolectric.annotation.RealObject;
+import org.robolectric.annotation.Resetter;
+import org.robolectric.res.Fs;
+import org.robolectric.shadow.api.Shadow;
+import org.robolectric.util.ReflectionHelpers;
+import org.robolectric.util.ReflectionHelpers.ClassParameter;
+
+/** Shadow for {@link Typeface}. */
+@Implements(value = Typeface.class, looseSignatures = true, isInAndroidSdk = false)
+@SuppressLint("NewApi")
+public class ShadowLegacyTypeface extends ShadowTypeface {
+  private static final Map<Long, FontDesc> FONTS = Collections.synchronizedMap(new HashMap<>());
+  private static final AtomicLong nextFontId = new AtomicLong(1);
+  private FontDesc description;
+
+  @HiddenApi
+  @Implementation(maxSdk = KITKAT)
+  protected void __constructor__(int fontId) {
+    description = findById((long) fontId);
+  }
+
+  @HiddenApi
+  @Implementation(minSdk = LOLLIPOP)
+  protected void __constructor__(long fontId) {
+    description = findById(fontId);
+  }
+
+  @Implementation
+  protected static void __staticInitializer__() {
+    Shadow.directInitialize(Typeface.class);
+    if (RuntimeEnvironment.getApiLevel() > R) {
+      Typeface.loadPreinstalledSystemFontMap();
+    }
+  }
+
+  @Implementation(minSdk = P)
+  protected static Typeface create(Typeface family, int weight, boolean italic) {
+    if (family == null) {
+      return createUnderlyingTypeface(null, weight);
+    } else {
+      ShadowTypeface shadowTypeface = Shadow.extract(family);
+      return createUnderlyingTypeface(shadowTypeface.getFontDescription().getFamilyName(), weight);
+    }
+  }
+
+  @Implementation
+  protected static Typeface create(String familyName, int style) {
+    return createUnderlyingTypeface(familyName, style);
+  }
+
+  @Implementation
+  protected static Typeface create(Typeface family, int style) {
+    if (family == null) {
+      return createUnderlyingTypeface(null, style);
+    } else {
+      ShadowTypeface shadowTypeface = Shadow.extract(family);
+      return createUnderlyingTypeface(shadowTypeface.getFontDescription().getFamilyName(), style);
+    }
+  }
+
+  @Implementation
+  protected static Typeface createFromAsset(AssetManager mgr, String path) {
+    ShadowAssetManager shadowAssetManager = Shadow.extract(mgr);
+    Collection<Path> assetDirs = shadowAssetManager.getAllAssetDirs();
+    for (Path assetDir : assetDirs) {
+      Path assetFile = assetDir.resolve(path);
+      if (Files.exists(assetFile)) {
+        return createUnderlyingTypeface(path, Typeface.NORMAL);
+      }
+
+      // maybe path is e.g. "myFont", but we should match "myFont.ttf" too?
+      Path[] files;
+      try {
+        files = Fs.listFiles(assetDir, f -> f.getFileName().toString().startsWith(path));
+      } catch (IOException e) {
+        throw new RuntimeException(e);
+      }
+      if (files.length != 0) {
+        return createUnderlyingTypeface(path, Typeface.NORMAL);
+      }
+    }
+
+    throw new RuntimeException("Font asset not found " + path);
+  }
+
+  @Implementation(minSdk = O, maxSdk = P)
+  protected static Typeface createFromResources(AssetManager mgr, String path, int cookie) {
+    return createUnderlyingTypeface(path, Typeface.NORMAL);
+  }
+
+  @Implementation(minSdk = O)
+  protected static Typeface createFromResources(
+      Object /* FamilyResourceEntry */ entry,
+      Object /* AssetManager */ mgr,
+      Object /* String */ path) {
+    return createUnderlyingTypeface((String) path, Typeface.NORMAL);
+  }
+
+  @Implementation
+  protected static Typeface createFromFile(File path) {
+    String familyName = path.toPath().getFileName().toString();
+    return createUnderlyingTypeface(familyName, Typeface.NORMAL);
+  }
+
+  @Implementation
+  protected static Typeface createFromFile(String path) {
+    return createFromFile(new File(path));
+  }
+
+  @Implementation
+  protected int getStyle() {
+    return description.getStyle();
+  }
+
+  @Override
+  @Implementation
+  public boolean equals(Object o) {
+    if (o instanceof Typeface) {
+      Typeface other = ((Typeface) o);
+      return Objects.equals(getFontDescription(), shadowOf(other).getFontDescription());
+    }
+    return false;
+  }
+
+  @Override
+  @Implementation
+  public int hashCode() {
+    return getFontDescription().hashCode();
+  }
+
+  @HiddenApi
+  @Implementation(minSdk = LOLLIPOP)
+  protected static Typeface createFromFamilies(Object /*FontFamily[]*/ families) {
+    return null;
+  }
+
+  @HiddenApi
+  @Implementation(minSdk = LOLLIPOP, maxSdk = N_MR1)
+  protected static Typeface createFromFamiliesWithDefault(Object /*FontFamily[]*/ families) {
+    return null;
+  }
+
+  @Implementation(minSdk = O, maxSdk = O_MR1)
+  protected static Typeface createFromFamiliesWithDefault(
+      Object /*FontFamily[]*/ families, Object /* int */ weight, Object /* int */ italic) {
+    return createUnderlyingTypeface("fake-font", Typeface.NORMAL);
+  }
+
+  @Implementation(minSdk = P)
+  protected static Typeface createFromFamiliesWithDefault(
+      Object /*FontFamily[]*/ families,
+      Object /* String */ fallbackName,
+      Object /* int */ weight,
+      Object /* int */ italic) {
+    return createUnderlyingTypeface((String) fallbackName, Typeface.NORMAL);
+  }
+
+  @Implementation(minSdk = P, maxSdk = P)
+  protected static void buildSystemFallback(
+      String xmlPath,
+      String fontDir,
+      ArrayMap<String, Typeface> fontMap,
+      ArrayMap<String, FontFamily[]> fallbackMap) {
+    fontMap.put("sans-serif", createUnderlyingTypeface("sans-serif", 0));
+  }
+
+  /** Avoid spurious error message about /system/etc/fonts.xml */
+  @Implementation(minSdk = LOLLIPOP, maxSdk = O_MR1)
+  protected static void init() {}
+
+  @HiddenApi
+  @Implementation(minSdk = Q, maxSdk = R)
+  protected static void initSystemDefaultTypefaces(
+      Object systemFontMap, Object fallbacks, Object aliases) {}
+
+  @Resetter
+  public static synchronized void reset() {
+    FONTS.clear();
+  }
+
+  protected static Typeface createUnderlyingTypeface(String familyName, int style) {
+    long thisFontId = nextFontId.getAndIncrement();
+    FONTS.put(thisFontId, new FontDesc(familyName, style));
+    if (getApiLevel() >= LOLLIPOP) {
+      return ReflectionHelpers.callConstructor(
+          Typeface.class, ClassParameter.from(long.class, thisFontId));
+    } else {
+      return ReflectionHelpers.callConstructor(
+          Typeface.class, ClassParameter.from(int.class, (int) thisFontId));
+    }
+  }
+
+  private static synchronized FontDesc findById(long fontId) {
+    if (FONTS.containsKey(fontId)) {
+      return FONTS.get(fontId);
+    }
+    throw new RuntimeException("Unknown font id: " + fontId);
+  }
+
+  @Implementation(minSdk = O, maxSdk = R)
+  protected static long nativeCreateFromArray(long[] familyArray, int weight, int italic) {
+    // TODO: implement this properly
+    long thisFontId = nextFontId.getAndIncrement();
+    FONTS.put(thisFontId, new FontDesc(null, weight));
+    return thisFontId;
+  }
+
+  /**
+   * Returns the font description.
+   *
+   * @return Font description.
+   */
+  @Override
+  public FontDesc getFontDescription() {
+    return description;
+  }
+
+  @Implementation(minSdk = S)
+  protected static void nativeForceSetStaticFinalField(String fieldname, Typeface typeface) {
+    ReflectionHelpers.setStaticField(Typeface.class, fieldname, typeface);
+  }
+
+  @Implementation(minSdk = S)
+  protected static long nativeCreateFromArray(
+      long[] familyArray, long fallbackTypeface, int weight, int italic) {
+    return ShadowLegacyTypeface.nativeCreateFromArray(familyArray, weight, italic);
+  }
+
+  /** Shadow for {@link Typeface.Builder} */
+  @Implements(value = Typeface.Builder.class, minSdk = Q)
+  public static class ShadowBuilder {
+    @RealObject Typeface.Builder realBuilder;
+
+    @Implementation
+    protected Typeface build() {
+      String path = ReflectionHelpers.getField(realBuilder, "mPath");
+      return createUnderlyingTypeface(path, Typeface.NORMAL);
+    }
+  }
+}
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowMatrix.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowMatrix.java
index ef26f9e..62b7021 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowMatrix.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowMatrix.java
@@ -1,29 +1,15 @@
 package org.robolectric.shadows;
 
-import static android.os.Build.VERSION_CODES.KITKAT;
-import static android.os.Build.VERSION_CODES.LOLLIPOP;
 
 import android.graphics.Matrix;
-import android.graphics.Matrix.ScaleToFit;
-import android.graphics.PointF;
-import android.graphics.RectF;
-import java.awt.geom.AffineTransform;
-import java.util.ArrayDeque;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Collections;
-import java.util.Deque;
-import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
-import java.util.Objects;
-import org.robolectric.annotation.Implementation;
 import org.robolectric.annotation.Implements;
-import org.robolectric.shadow.api.Shadow;
+import org.robolectric.shadows.ShadowMatrix.Picker;
 
 @SuppressWarnings({"UnusedDeclaration"})
-@Implements(Matrix.class)
-public class ShadowMatrix {
+@Implements(value = Matrix.class, shadowPicker = Picker.class)
+public abstract class ShadowMatrix {
   public static final String TRANSLATE = "translate";
   public static final String SCALE = "scale";
   public static final String ROTATE = "rotate";
@@ -31,631 +17,35 @@
   public static final String SKEW = "skew";
   public static final String MATRIX = "matrix";
 
-  private static final float EPSILON = 1e-3f;
-
-  private final Deque<String> preOps = new ArrayDeque<>();
-  private final Deque<String> postOps = new ArrayDeque<>();
-  private final Map<String, String> setOps = new LinkedHashMap<>();
-
-  private SimpleMatrix simpleMatrix = SimpleMatrix.newIdentityMatrix();
-
-  @Implementation
-  protected void __constructor__(Matrix src) {
-    set(src);
-  }
-
   /**
-   * A list of all 'pre' operations performed on this Matrix. The last operation performed will
-   * be first in the list.
+   * A list of all 'pre' operations performed on this Matrix. The last operation performed will be
+   * first in the list.
+   *
    * @return A list of all 'pre' operations performed on this Matrix.
    */
-  public List<String> getPreOperations() {
-    return Collections.unmodifiableList(new ArrayList<>(preOps));
-  }
+  public abstract List<String> getPreOperations();
 
   /**
-   * A list of all 'post' operations performed on this Matrix. The last operation performed will
-   * be last in the list.
+   * A list of all 'post' operations performed on this Matrix. The last operation performed will be
+   * last in the list.
+   *
    * @return A list of all 'post' operations performed on this Matrix.
    */
-  public List<String> getPostOperations() {
-    return Collections.unmodifiableList(new ArrayList<>(postOps));
-  }
+  public abstract List<String> getPostOperations();
 
   /**
    * A map of all 'set' operations performed on this Matrix.
+   *
    * @return A map of all 'set' operations performed on this Matrix.
    */
-  public Map<String, String> getSetOperations() {
-    return Collections.unmodifiableMap(new LinkedHashMap<>(setOps));
-  }
+  public abstract Map<String, String> getSetOperations();
 
-  @Implementation
-  protected boolean isIdentity() {
-    return simpleMatrix.equals(SimpleMatrix.IDENTITY);
-  }
+  public abstract String getDescription();
 
-  @Implementation(minSdk = LOLLIPOP)
-  protected boolean isAffine() {
-    return simpleMatrix.isAffine();
-  }
-
-  @Implementation
-  protected boolean rectStaysRect() {
-    return simpleMatrix.rectStaysRect();
-  }
-
-  @Implementation
-  protected void getValues(float[] values) {
-    simpleMatrix.getValues(values);
-  }
-
-  @Implementation
-  protected void setValues(float[] values) {
-    simpleMatrix = new SimpleMatrix(values);
-  }
-
-  @Implementation
-  protected void set(Matrix src) {
-    reset();
-    if (src != null) {
-      ShadowMatrix shadowMatrix = Shadow.extract(src);
-      preOps.addAll(shadowMatrix.preOps);
-      postOps.addAll(shadowMatrix.postOps);
-      setOps.putAll(shadowMatrix.setOps);
-      simpleMatrix = new SimpleMatrix(getSimpleMatrix(src));
-    }
-  }
-
-  @Implementation
-  protected void reset() {
-    preOps.clear();
-    postOps.clear();
-    setOps.clear();
-    simpleMatrix = SimpleMatrix.newIdentityMatrix();
-  }
-
-  @Implementation
-  protected void setTranslate(float dx, float dy) {
-    setOps.put(TRANSLATE, dx + " " + dy);
-    simpleMatrix = SimpleMatrix.translate(dx, dy);
-  }
-
-  @Implementation
-  protected void setScale(float sx, float sy, float px, float py) {
-    setOps.put(SCALE, sx + " " + sy + " " + px + " " + py);
-    simpleMatrix = SimpleMatrix.scale(sx, sy, px, py);
-  }
-
-  @Implementation
-  protected void setScale(float sx, float sy) {
-    setOps.put(SCALE, sx + " " + sy);
-    simpleMatrix = SimpleMatrix.scale(sx, sy);
-  }
-
-  @Implementation
-  protected void setRotate(float degrees, float px, float py) {
-    setOps.put(ROTATE, degrees + " " + px + " " + py);
-    simpleMatrix = SimpleMatrix.rotate(degrees, px, py);
-  }
-
-  @Implementation
-  protected void setRotate(float degrees) {
-    setOps.put(ROTATE, Float.toString(degrees));
-    simpleMatrix = SimpleMatrix.rotate(degrees);
-  }
-
-  @Implementation
-  protected void setSinCos(float sinValue, float cosValue, float px, float py) {
-    setOps.put(SINCOS, sinValue + " " + cosValue + " " + px + " " + py);
-    simpleMatrix = SimpleMatrix.sinCos(sinValue, cosValue, px, py);
-  }
-
-  @Implementation
-  protected void setSinCos(float sinValue, float cosValue) {
-    setOps.put(SINCOS, sinValue + " " + cosValue);
-    simpleMatrix = SimpleMatrix.sinCos(sinValue, cosValue);
-  }
-
-  @Implementation
-  protected void setSkew(float kx, float ky, float px, float py) {
-    setOps.put(SKEW, kx + " " + ky + " " + px + " " + py);
-    simpleMatrix = SimpleMatrix.skew(kx, ky, px, py);
-  }
-
-  @Implementation
-  protected void setSkew(float kx, float ky) {
-    setOps.put(SKEW, kx + " " + ky);
-    simpleMatrix = SimpleMatrix.skew(kx, ky);
-  }
-
-  @Implementation
-  protected boolean setConcat(Matrix a, Matrix b) {
-    simpleMatrix = getSimpleMatrix(a).multiply(getSimpleMatrix(b));
-    return true;
-  }
-
-  @Implementation
-  protected boolean preTranslate(float dx, float dy) {
-    preOps.addFirst(TRANSLATE + " " + dx + " " + dy);
-    return preConcat(SimpleMatrix.translate(dx, dy));
-  }
-
-  @Implementation
-  protected boolean preScale(float sx, float sy, float px, float py) {
-    preOps.addFirst(SCALE + " " + sx + " " + sy + " " + px + " " + py);
-    return preConcat(SimpleMatrix.scale(sx, sy, px, py));
-  }
-
-  @Implementation
-  protected boolean preScale(float sx, float sy) {
-    preOps.addFirst(SCALE + " " + sx + " " + sy);
-    return preConcat(SimpleMatrix.scale(sx, sy));
-  }
-
-  @Implementation
-  protected boolean preRotate(float degrees, float px, float py) {
-    preOps.addFirst(ROTATE + " " + degrees + " " + px + " " + py);
-    return preConcat(SimpleMatrix.rotate(degrees, px, py));
-  }
-
-  @Implementation
-  protected boolean preRotate(float degrees) {
-    preOps.addFirst(ROTATE + " " + Float.toString(degrees));
-    return preConcat(SimpleMatrix.rotate(degrees));
-  }
-
-  @Implementation
-  protected boolean preSkew(float kx, float ky, float px, float py) {
-    preOps.addFirst(SKEW + " " + kx + " " + ky + " " + px + " " + py);
-    return preConcat(SimpleMatrix.skew(kx, ky, px, py));
-  }
-
-  @Implementation
-  protected boolean preSkew(float kx, float ky) {
-    preOps.addFirst(SKEW + " " + kx + " " + ky);
-    return preConcat(SimpleMatrix.skew(kx, ky));
-  }
-
-  @Implementation
-  protected boolean preConcat(Matrix other) {
-    preOps.addFirst(MATRIX + " " + other);
-    return preConcat(getSimpleMatrix(other));
-  }
-
-  @Implementation
-  protected boolean postTranslate(float dx, float dy) {
-    postOps.addLast(TRANSLATE + " " + dx + " " + dy);
-    return postConcat(SimpleMatrix.translate(dx, dy));
-  }
-
-  @Implementation
-  protected boolean postScale(float sx, float sy, float px, float py) {
-    postOps.addLast(SCALE + " " + sx + " " + sy + " " + px + " " + py);
-    return postConcat(SimpleMatrix.scale(sx, sy, px, py));
-  }
-
-  @Implementation
-  protected boolean postScale(float sx, float sy) {
-    postOps.addLast(SCALE + " " + sx + " " + sy);
-    return postConcat(SimpleMatrix.scale(sx, sy));
-  }
-
-  @Implementation
-  protected boolean postRotate(float degrees, float px, float py) {
-    postOps.addLast(ROTATE + " " + degrees + " " + px + " " + py);
-    return postConcat(SimpleMatrix.rotate(degrees, px, py));
-  }
-
-  @Implementation
-  protected boolean postRotate(float degrees) {
-    postOps.addLast(ROTATE + " " + Float.toString(degrees));
-    return postConcat(SimpleMatrix.rotate(degrees));
-  }
-
-  @Implementation
-  protected boolean postSkew(float kx, float ky, float px, float py) {
-    postOps.addLast(SKEW + " " + kx + " " + ky + " " + px + " " + py);
-    return postConcat(SimpleMatrix.skew(kx, ky, px, py));
-  }
-
-  @Implementation
-  protected boolean postSkew(float kx, float ky) {
-    postOps.addLast(SKEW + " " + kx + " " + ky);
-    return postConcat(SimpleMatrix.skew(kx, ky));
-  }
-
-  @Implementation
-  protected boolean postConcat(Matrix other) {
-    postOps.addLast(MATRIX + " " + other);
-    return postConcat(getSimpleMatrix(other));
-  }
-
-  @Implementation
-  protected boolean invert(Matrix inverse) {
-    final SimpleMatrix inverseMatrix = simpleMatrix.invert();
-    if (inverseMatrix != null) {
-      if (inverse != null) {
-        final ShadowMatrix shadowInverse = Shadow.extract(inverse);
-        shadowInverse.simpleMatrix = inverseMatrix;
-      }
-      return true;
-    }
-    return false;
-  }
-
-  boolean hasPerspective() {
-    return (simpleMatrix.mValues[6] != 0 || simpleMatrix.mValues[7] != 0 || simpleMatrix.mValues[8] != 1);
-  }
-
-  protected AffineTransform getAffineTransform() {
-    // the AffineTransform constructor takes the value in a different order
-    // for a matrix [ 0 1 2 ]
-    //              [ 3 4 5 ]
-    // the order is 0, 3, 1, 4, 2, 5...
-    return new AffineTransform(
-        simpleMatrix.mValues[0],
-        simpleMatrix.mValues[3],
-        simpleMatrix.mValues[1],
-        simpleMatrix.mValues[4],
-        simpleMatrix.mValues[2],
-        simpleMatrix.mValues[5]);
-  }
-
-  public PointF mapPoint(float x, float y) {
-    return simpleMatrix.transform(new PointF(x, y));
-  }
-
-  public PointF mapPoint(PointF point) {
-    return simpleMatrix.transform(point);
-  }
-
-  @Implementation
-  protected boolean mapRect(RectF destination, RectF source) {
-    final PointF leftTop = mapPoint(source.left, source.top);
-    final PointF rightBottom = mapPoint(source.right, source.bottom);
-    destination.set(
-        Math.min(leftTop.x, rightBottom.x),
-        Math.min(leftTop.y, rightBottom.y),
-        Math.max(leftTop.x, rightBottom.x),
-        Math.max(leftTop.y, rightBottom.y));
-    return true;
-  }
-
-  @Implementation
-  protected void mapPoints(float[] dst, int dstIndex, float[] src, int srcIndex, int pointCount) {
-    for (int i = 0; i < pointCount; i++) {
-      final PointF mapped = mapPoint(src[srcIndex + i * 2], src[srcIndex + i * 2 + 1]);
-      dst[dstIndex + i * 2] = mapped.x;
-      dst[dstIndex + i * 2 + 1] = mapped.y;
-    }
-  }
-
-  @Implementation
-  protected void mapVectors(float[] dst, int dstIndex, float[] src, int srcIndex, int vectorCount) {
-    final float transX = simpleMatrix.mValues[Matrix.MTRANS_X];
-    final float transY = simpleMatrix.mValues[Matrix.MTRANS_Y];
-
-    simpleMatrix.mValues[Matrix.MTRANS_X] = 0;
-    simpleMatrix.mValues[Matrix.MTRANS_Y] = 0;
-
-    for (int i = 0; i < vectorCount; i++) {
-      final PointF mapped = mapPoint(src[srcIndex + i * 2], src[srcIndex + i * 2 + 1]);
-      dst[dstIndex + i * 2] = mapped.x;
-      dst[dstIndex + i * 2 + 1] = mapped.y;
-    }
-
-    simpleMatrix.mValues[Matrix.MTRANS_X] = transX;
-    simpleMatrix.mValues[Matrix.MTRANS_Y] = transY;
-  }
-
-  @Implementation
-  protected float mapRadius(float radius) {
-    float[] src = new float[] {radius, 0.f, 0.f, radius};
-    mapVectors(src, 0, src, 0, 2);
-
-    float l1 = (float) Math.hypot(src[0], src[1]);
-    float l2 = (float) Math.hypot(src[2], src[3]);
-    return (float) Math.sqrt(l1 * l2);
-  }
-
-  @Implementation
-  protected boolean setRectToRect(RectF src, RectF dst, Matrix.ScaleToFit stf) {
-    if (src.isEmpty()) {
-      reset();
-      return false;
-    }
-    return simpleMatrix.setRectToRect(src, dst, stf);
-  }
-
-  @Implementation
-  @Override
-  public boolean equals(Object obj) {
-    if (obj instanceof Matrix) {
-        return getSimpleMatrix(((Matrix) obj)).equals(simpleMatrix);
-    } else {
-        return obj instanceof ShadowMatrix && obj.equals(simpleMatrix);
-    }
-  }
-
-  @Implementation(minSdk = KITKAT)
-  @Override
-  public int hashCode() {
-      return Objects.hashCode(simpleMatrix);
-  }
-
-  public String getDescription() {
-    return "Matrix[pre=" + preOps + ", set=" + setOps + ", post=" + postOps + "]";
-  }
-
-  private static SimpleMatrix getSimpleMatrix(Matrix matrix) {
-    final ShadowMatrix otherMatrix = Shadow.extract(matrix);
-    return otherMatrix.simpleMatrix;
-  }
-
-  private boolean postConcat(SimpleMatrix matrix) {
-    simpleMatrix = matrix.multiply(simpleMatrix);
-    return true;
-  }
-
-  private boolean preConcat(SimpleMatrix matrix) {
-    simpleMatrix = simpleMatrix.multiply(matrix);
-    return true;
-  }
-
-  /**
-   * A simple implementation of an immutable matrix.
-   */
-  private static class SimpleMatrix {
-    private static final SimpleMatrix IDENTITY = newIdentityMatrix();
-
-    private static SimpleMatrix newIdentityMatrix() {
-      return new SimpleMatrix(
-          new float[] {
-            1.0f, 0.0f, 0.0f,
-            0.0f, 1.0f, 0.0f,
-            0.0f, 0.0f, 1.0f,
-          });
-    }
-
-    private final float[] mValues;
-
-    SimpleMatrix(SimpleMatrix matrix) {
-      mValues = Arrays.copyOf(matrix.mValues, matrix.mValues.length);
-    }
-
-    private SimpleMatrix(float[] values) {
-      if (values.length != 9) {
-        throw new ArrayIndexOutOfBoundsException();
-      }
-      mValues = Arrays.copyOf(values, 9);
-    }
-
-    public boolean isAffine() {
-      return mValues[6] == 0.0f && mValues[7] == 0.0f && mValues[8] == 1.0f;
-    }
-
-    public boolean rectStaysRect() {
-      final float m00 = mValues[0];
-      final float m01 = mValues[1];
-      final float m10 = mValues[3];
-      final float m11 = mValues[4];
-      return (m00 == 0 && m11 == 0 && m01 != 0 && m10 != 0) || (m00 != 0 && m11 != 0 && m01 == 0 && m10 == 0);
-    }
-
-    public void getValues(float[] values) {
-      if (values.length < 9) {
-        throw new ArrayIndexOutOfBoundsException();
-      }
-      System.arraycopy(mValues, 0, values, 0, 9);
-    }
-
-    public static SimpleMatrix translate(float dx, float dy) {
-      return new SimpleMatrix(new float[] {
-          1.0f, 0.0f, dx,
-          0.0f, 1.0f, dy,
-          0.0f, 0.0f, 1.0f,
-      });
-    }
-
-    public static SimpleMatrix scale(float sx, float sy, float px, float py) {
-      return new SimpleMatrix(new float[] {
-          sx,   0.0f, px * (1 - sx),
-          0.0f, sy,   py * (1 - sy),
-          0.0f, 0.0f, 1.0f,
-      });
-    }
-
-    public static SimpleMatrix scale(float sx, float sy) {
-      return new SimpleMatrix(new float[] {
-          sx,   0.0f, 0.0f,
-          0.0f, sy,   0.0f,
-          0.0f, 0.0f, 1.0f,
-      });
-    }
-
-    public static SimpleMatrix rotate(float degrees, float px, float py) {
-      final double radians = Math.toRadians(degrees);
-      final float sin = (float) Math.sin(radians);
-      final float cos = (float) Math.cos(radians);
-      return sinCos(sin, cos, px, py);
-    }
-
-    public static SimpleMatrix rotate(float degrees) {
-      final double radians = Math.toRadians(degrees);
-      final float sin = (float) Math.sin(radians);
-      final float cos = (float) Math.cos(radians);
-      return sinCos(sin, cos);
-    }
-
-    public static SimpleMatrix sinCos(float sin, float cos, float px, float py) {
-      return new SimpleMatrix(new float[] {
-          cos,  -sin, sin * py + (1 - cos) * px,
-          sin,  cos,  -sin * px + (1 - cos) * py,
-          0.0f, 0.0f, 1.0f,
-      });
-    }
-
-    public static SimpleMatrix sinCos(float sin, float cos) {
-      return new SimpleMatrix(new float[] {
-          cos,  -sin, 0.0f,
-          sin,  cos,  0.0f,
-          0.0f, 0.0f, 1.0f,
-      });
-    }
-
-    public static SimpleMatrix skew(float kx, float ky, float px, float py) {
-      return new SimpleMatrix(new float[] {
-          1.0f, kx,   -kx * py,
-          ky,   1.0f, -ky * px,
-          0.0f, 0.0f, 1.0f,
-      });
-    }
-
-    public static SimpleMatrix skew(float kx, float ky) {
-      return new SimpleMatrix(new float[] {
-          1.0f, kx,   0.0f,
-          ky,   1.0f, 0.0f,
-          0.0f, 0.0f, 1.0f,
-      });
-    }
-
-    public SimpleMatrix multiply(SimpleMatrix matrix) {
-      final float[] values = new float[9];
-      for (int i = 0; i < values.length; ++i) {
-        final int row = i / 3;
-        final int col = i % 3;
-        for (int j = 0; j < 3; ++j) {
-          values[i] += mValues[row * 3 + j] * matrix.mValues[j * 3 + col];
-        }
-      }
-      return new SimpleMatrix(values);
-    }
-
-    public SimpleMatrix invert() {
-      final float invDet = inverseDeterminant();
-      if (invDet == 0) {
-        return null;
-      }
-
-      final float[] src = mValues;
-      final float[] dst = new float[9];
-      dst[0] = cross_scale(src[4], src[8], src[5], src[7], invDet);
-      dst[1] = cross_scale(src[2], src[7], src[1], src[8], invDet);
-      dst[2] = cross_scale(src[1], src[5], src[2], src[4], invDet);
-
-      dst[3] = cross_scale(src[5], src[6], src[3], src[8], invDet);
-      dst[4] = cross_scale(src[0], src[8], src[2], src[6], invDet);
-      dst[5] = cross_scale(src[2], src[3], src[0], src[5], invDet);
-
-      dst[6] = cross_scale(src[3], src[7], src[4], src[6], invDet);
-      dst[7] = cross_scale(src[1], src[6], src[0], src[7], invDet);
-      dst[8] = cross_scale(src[0], src[4], src[1], src[3], invDet);
-      return new SimpleMatrix(dst);
-    }
-
-    public PointF transform(PointF point) {
-      return new PointF(
-          point.x * mValues[0] + point.y * mValues[1] + mValues[2],
-          point.x * mValues[3] + point.y * mValues[4] + mValues[5]);
-    }
-
-    // See: https://android.googlesource.com/platform/frameworks/base/+/6fca81de9b2079ec88e785f58bf49bf1f0c105e2/tools/layoutlib/bridge/src/android/graphics/Matrix_Delegate.java
-    protected boolean setRectToRect(RectF src, RectF dst, ScaleToFit stf) {
-      if (dst.isEmpty()) {
-        mValues[0] =
-            mValues[1] =
-                mValues[2] = mValues[3] = mValues[4] = mValues[5] = mValues[6] = mValues[7] = 0;
-        mValues[8] = 1;
-      } else {
-        float tx = dst.width() / src.width();
-        float sx = dst.width() / src.width();
-        float ty = dst.height() / src.height();
-        float sy = dst.height() / src.height();
-        boolean xLarger = false;
-
-        if (stf != ScaleToFit.FILL) {
-          if (sx > sy) {
-            xLarger = true;
-            sx = sy;
-          } else {
-            sy = sx;
-          }
-        }
-
-        tx = dst.left - src.left * sx;
-        ty = dst.top - src.top * sy;
-        if (stf == ScaleToFit.CENTER || stf == ScaleToFit.END) {
-          float diff;
-
-          if (xLarger) {
-            diff = dst.width() - src.width() * sy;
-          } else {
-            diff = dst.height() - src.height() * sy;
-          }
-
-          if (stf == ScaleToFit.CENTER) {
-            diff = diff / 2;
-          }
-
-          if (xLarger) {
-            tx += diff;
-          } else {
-            ty += diff;
-          }
-        }
-
-        mValues[0] = sx;
-        mValues[4] = sy;
-        mValues[2] = tx;
-        mValues[5] = ty;
-        mValues[1] = mValues[3] = mValues[6] = mValues[7] = 0;
-      }
-      // shared cleanup
-      mValues[8] = 1;
-      return true;
-    }
-
-    @Override
-    public boolean equals(Object o) {
-      return this == o || (o instanceof SimpleMatrix && equals((SimpleMatrix) o));
-    }
-
-    @SuppressWarnings("NonOverridingEquals")
-    public boolean equals(SimpleMatrix matrix) {
-      if (matrix == null) {
-        return false;
-      }
-      for (int i = 0; i < mValues.length; i++) {
-        if (!isNearlyZero(matrix.mValues[i] - mValues[i])) {
-          return false;
-        }
-      }
-      return true;
-    }
-
-    @Override
-    public int hashCode() {
-      return Arrays.hashCode(mValues);
-    }
-
-    private static boolean isNearlyZero(float value) {
-      return Math.abs(value) < EPSILON;
-    }
-
-    private static float cross(float a, float b, float c, float d) {
-      return a * b - c * d;
-    }
-
-    private static float cross_scale(float a, float b, float c, float d, float scale) {
-      return cross(a, b, c, d) * scale;
-    }
-
-    private float inverseDeterminant() {
-      final float determinant = mValues[0] * cross(mValues[4], mValues[8], mValues[5], mValues[7]) +
-          mValues[1] * cross(mValues[5], mValues[6], mValues[3], mValues[8]) +
-          mValues[2] * cross(mValues[3], mValues[7], mValues[4], mValues[6]);
-      return isNearlyZero(determinant) ? 0.0f : 1.0f / determinant;
+  /** Shadow picker for {@link Matrix}. */
+  public static final class Picker extends GraphicsShadowPicker<Object> {
+    public Picker() {
+      super(ShadowLegacyMatrix.class, ShadowNativeMatrix.class);
     }
   }
 }
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowMediaController.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowMediaController.java
index 809b5cc..252bc42 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowMediaController.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowMediaController.java
@@ -4,6 +4,7 @@
 import static org.robolectric.util.reflector.Reflector.reflector;
 
 import android.annotation.NonNull;
+import android.annotation.Nullable;
 import android.app.PendingIntent;
 import android.media.MediaMetadata;
 import android.media.Rating;
@@ -12,6 +13,7 @@
 import android.media.session.MediaController.PlaybackInfo;
 import android.media.session.PlaybackState;
 import android.os.Bundle;
+import android.os.Handler;
 import java.util.ArrayList;
 import java.util.List;
 import org.robolectric.annotation.Implementation;
@@ -30,6 +32,7 @@
   private PlaybackInfo playbackInfo;
   private MediaMetadata mediaMetadata;
   private PendingIntent sessionActivity;
+  private Bundle extras;
 
   /**
    * A value of RATING_NONE for ratingType indicates that rating media is not supported by the media
@@ -122,14 +125,26 @@
     return sessionActivity;
   }
 
+  /** Saves the extras to control the return value of {@link MediaController#getExtras()}. */
+  public void setExtras(Bundle extras) {
+    this.extras = extras;
+  }
+
+  /** Gets the extras set via {@link #extras}. */
+  @Implementation
+  protected Bundle getExtras() {
+    return extras;
+  }
+
   /**
    * Register callback and store it in the shadow to make it easier to check the state of the
-   * registered callbacks.
+   * registered callbacks. Handler is just passed on to the real class.
    */
   @Implementation
-  protected void registerCallback(@NonNull Callback callback) {
+  protected void registerCallback(@NonNull Callback callback, @Nullable Handler handler) {
     callbacks.add(callback);
-    reflector(MediaControllerReflector.class, realMediaController).registerCallback(callback);
+    reflector(MediaControllerReflector.class, realMediaController)
+        .registerCallback(callback, handler);
   }
 
   /**
@@ -192,7 +207,7 @@
   interface MediaControllerReflector {
 
     @Direct
-    void registerCallback(@NonNull Callback callback);
+    void registerCallback(@NonNull Callback callback, @Nullable Handler handler);
 
     @Direct
     void unregisterCallback(@NonNull Callback callback);
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowMediaStore.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowMediaStore.java
index be962b3..31fc796 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowMediaStore.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowMediaStore.java
@@ -35,7 +35,11 @@
 
       @Implementation
       protected static Bitmap getBitmap(ContentResolver cr, Uri url) {
-        return ShadowBitmapFactory.create(url.toString(), null, null);
+        if (ShadowView.useRealGraphics()) {
+          return Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888);
+        } else {
+          return ShadowBitmapFactory.create(url.toString(), null, null);
+        }
       }
     }
 
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeAllocationRegistry.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeAllocationRegistry.java
index dfe78a2..ab3c6e3 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeAllocationRegistry.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeAllocationRegistry.java
@@ -1,21 +1,65 @@
 package org.robolectric.shadows;
 
-import static android.os.Build.VERSION_CODES.N;
+import static android.os.Build.VERSION_CODES.O;
+import static org.robolectric.util.reflector.Reflector.reflector;
 
 import libcore.util.NativeAllocationRegistry;
 import org.robolectric.annotation.Implementation;
 import org.robolectric.annotation.Implements;
+import org.robolectric.annotation.RealObject;
+import org.robolectric.nativeruntime.NativeAllocationRegistryNatives;
+import org.robolectric.shadows.ShadowNativeAllocationRegistry.Picker;
+import org.robolectric.util.reflector.Accessor;
+import org.robolectric.util.reflector.Direct;
+import org.robolectric.util.reflector.ForType;
 
-@Implements(value = NativeAllocationRegistry.class, minSdk = N, isInAndroidSdk = false, looseSignatures = true)
+/** Shadow for {@link NativeAllocationRegistry} that is backed by native code */
+@Implements(
+    value = NativeAllocationRegistry.class,
+    minSdk = O,
+    isInAndroidSdk = false,
+    shadowPicker = Picker.class)
 public class ShadowNativeAllocationRegistry {
 
-  @Implementation
-  protected Runnable registerNativeAllocation(Object referent, Object allocator) {
-    return () -> {};
-  }
+  @RealObject protected NativeAllocationRegistry realNativeAllocationRegistry;
 
   @Implementation
   protected Runnable registerNativeAllocation(Object referent, long nativePtr) {
-    return () -> {};
+    // Avoid registering native allocations for classes where native methods are no-ops (like
+    // Binder), or for classes that simulate native pointers (like binary resources) but don't
+    // actually use native libraries.
+    if (nativePtr != 0 && hasValidFreeFunction()) {
+      return reflector(NativeAllocationRegistryReflector.class, realNativeAllocationRegistry)
+          .registerNativeAllocation(referent, nativePtr);
+    } else {
+      return () -> {};
+    }
+  }
+
+  private boolean hasValidFreeFunction() {
+    return reflector(NativeAllocationRegistryReflector.class, realNativeAllocationRegistry)
+            .getFreeFunction()
+        != 0;
+  }
+
+  @Implementation
+  protected static void applyFreeFunction(long freeFunction, long nativePtr) {
+    NativeAllocationRegistryNatives.applyFreeFunction(freeFunction, nativePtr);
+  }
+
+  @ForType(NativeAllocationRegistry.class)
+  interface NativeAllocationRegistryReflector {
+    @Direct
+    Runnable registerNativeAllocation(Object referent, long nativePtr);
+
+    @Accessor("freeFunction")
+    long getFreeFunction();
+  }
+
+  /** Shadow picker for {@link NativeAllocationRegistry}. */
+  public static final class Picker extends GraphicsShadowPicker<Object> {
+    public Picker() {
+      super(ShadowNoopNativeAllocationRegistry.class, ShadowNativeAllocationRegistry.class);
+    }
   }
 }
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeAnimatedImageDrawable.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeAnimatedImageDrawable.java
new file mode 100644
index 0000000..a73f0a6
--- /dev/null
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeAnimatedImageDrawable.java
@@ -0,0 +1,125 @@
+package org.robolectric.shadows;
+
+import static android.os.Build.VERSION_CODES.P;
+import static android.os.Build.VERSION_CODES.Q;
+import static android.os.Build.VERSION_CODES.S;
+import static android.os.Build.VERSION_CODES.S_V2;
+import static android.os.Build.VERSION_CODES.TIRAMISU;
+
+import android.graphics.ImageDecoder;
+import android.graphics.Rect;
+import android.graphics.drawable.AnimatedImageDrawable;
+import java.io.IOException;
+import java.lang.ref.WeakReference;
+import org.robolectric.annotation.Implementation;
+import org.robolectric.annotation.Implements;
+import org.robolectric.nativeruntime.AnimatedImageDrawableNatives;
+import org.robolectric.shadows.ShadowNativeAnimatedImageDrawable.Picker;
+
+/** Shadow for {@link AnimatedImageDrawable} that is backed by native code */
+@Implements(value = AnimatedImageDrawable.class, shadowPicker = Picker.class, minSdk = P)
+public class ShadowNativeAnimatedImageDrawable extends ShadowDrawable {
+  @Implementation(minSdk = Q)
+  protected static long nCreate(
+      long nativeImageDecoder,
+      ImageDecoder decoder,
+      int width,
+      int height,
+      long colorSpaceHandle,
+      boolean extended,
+      Rect cropRect)
+      throws IOException {
+    return AnimatedImageDrawableNatives.nCreate(
+        nativeImageDecoder, decoder, width, height, colorSpaceHandle, extended, cropRect);
+  }
+
+  @Implementation(minSdk = P, maxSdk = P)
+  protected static long nCreate(
+      long nativeImageDecoder, ImageDecoder decoder, int width, int height, Rect cropRect)
+      throws IOException {
+    return nCreate(nativeImageDecoder, decoder, width, height, 0, false, cropRect);
+  }
+
+  @Implementation
+  protected static long nGetNativeFinalizer() {
+    return AnimatedImageDrawableNatives.nGetNativeFinalizer();
+  }
+
+  @Implementation
+  protected static long nDraw(long nativePtr, long canvasNativePtr) {
+    return AnimatedImageDrawableNatives.nDraw(nativePtr, canvasNativePtr);
+  }
+
+  @Implementation
+  protected static void nSetAlpha(long nativePtr, int alpha) {
+    AnimatedImageDrawableNatives.nSetAlpha(nativePtr, alpha);
+  }
+
+  @Implementation
+  protected static int nGetAlpha(long nativePtr) {
+    return AnimatedImageDrawableNatives.nGetAlpha(nativePtr);
+  }
+
+  @Implementation
+  protected static void nSetColorFilter(long nativePtr, long nativeFilter) {
+    AnimatedImageDrawableNatives.nSetColorFilter(nativePtr, nativeFilter);
+  }
+
+  @Implementation
+  protected static boolean nIsRunning(long nativePtr) {
+    return AnimatedImageDrawableNatives.nIsRunning(nativePtr);
+  }
+
+  @Implementation
+  protected static boolean nStart(long nativePtr) {
+    return AnimatedImageDrawableNatives.nStart(nativePtr);
+  }
+
+  @Implementation
+  protected static boolean nStop(long nativePtr) {
+    return AnimatedImageDrawableNatives.nStop(nativePtr);
+  }
+
+  @Implementation
+  protected static int nGetRepeatCount(long nativePtr) {
+    return AnimatedImageDrawableNatives.nGetRepeatCount(nativePtr);
+  }
+
+  @Implementation
+  protected static void nSetRepeatCount(long nativePtr, int repeatCount) {
+    AnimatedImageDrawableNatives.nSetRepeatCount(nativePtr, repeatCount);
+  }
+
+  @Implementation(maxSdk = S_V2)
+  protected static void nSetOnAnimationEndListener(long nativePtr, AnimatedImageDrawable drawable) {
+    AnimatedImageDrawableNatives.nSetOnAnimationEndListener(nativePtr, drawable);
+  }
+
+  @Implementation(minSdk = TIRAMISU)
+  protected static void nSetOnAnimationEndListener(
+      long nativePtr, WeakReference<AnimatedImageDrawable> drawable) {
+    AnimatedImageDrawableNatives.nSetOnAnimationEndListener(nativePtr, drawable.get());
+  }
+
+  @Implementation
+  protected static long nNativeByteSize(long nativePtr) {
+    return AnimatedImageDrawableNatives.nNativeByteSize(nativePtr);
+  }
+
+  @Implementation
+  protected static void nSetMirrored(long nativePtr, boolean mirror) {
+    AnimatedImageDrawableNatives.nSetMirrored(nativePtr, mirror);
+  }
+
+  @Implementation(minSdk = S)
+  protected static void nSetBounds(long nativePtr, Rect rect) {
+    AnimatedImageDrawableNatives.nSetBounds(nativePtr, rect);
+  }
+
+  /** Shadow picker for {@link AnimatedImageDrawable}. */
+  public static final class Picker extends GraphicsShadowPicker<Object> {
+    public Picker() {
+      super(null, ShadowNativeAnimatedImageDrawable.class);
+    }
+  }
+}
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeAnimatedVectorDrawable.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeAnimatedVectorDrawable.java
new file mode 100644
index 0000000..79b1eaf
--- /dev/null
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeAnimatedVectorDrawable.java
@@ -0,0 +1,120 @@
+package org.robolectric.shadows;
+
+import static android.os.Build.VERSION_CODES.N;
+import static android.os.Build.VERSION_CODES.N_MR1;
+import static android.os.Build.VERSION_CODES.O;
+
+import android.graphics.drawable.AnimatedVectorDrawable;
+import android.graphics.drawable.AnimatedVectorDrawable.VectorDrawableAnimatorRT;
+import org.robolectric.annotation.Implementation;
+import org.robolectric.annotation.Implements;
+import org.robolectric.nativeruntime.AnimatedVectorDrawableNatives;
+import org.robolectric.nativeruntime.DefaultNativeRuntimeLoader;
+import org.robolectric.shadows.ShadowNativeAnimatedVectorDrawable.Picker;
+
+/** Shadow for {@link AnimatedVectorDrawable} that is backed by native code */
+@Implements(value = AnimatedVectorDrawable.class, minSdk = O, shadowPicker = Picker.class)
+public class ShadowNativeAnimatedVectorDrawable extends ShadowDrawable {
+
+  @Implementation(minSdk = N)
+  protected static long nCreateAnimatorSet() {
+    DefaultNativeRuntimeLoader.injectAndLoad();
+    return AnimatedVectorDrawableNatives.nCreateAnimatorSet();
+  }
+
+  @Implementation(minSdk = N_MR1)
+  protected static void nSetVectorDrawableTarget(long animatorPtr, long vectorDrawablePtr) {
+    AnimatedVectorDrawableNatives.nSetVectorDrawableTarget(animatorPtr, vectorDrawablePtr);
+  }
+
+  @Implementation(minSdk = N_MR1)
+  protected static void nAddAnimator(
+      long setPtr,
+      long propertyValuesHolder,
+      long nativeInterpolator,
+      long startDelay,
+      long duration,
+      int repeatCount,
+      int repeatMode) {
+    AnimatedVectorDrawableNatives.nAddAnimator(
+        setPtr,
+        propertyValuesHolder,
+        nativeInterpolator,
+        startDelay,
+        duration,
+        repeatCount,
+        repeatMode);
+  }
+
+  @Implementation(minSdk = N)
+  protected static void nSetPropertyHolderData(long nativePtr, float[] data, int length) {
+    AnimatedVectorDrawableNatives.nSetPropertyHolderData(nativePtr, data, length);
+  }
+
+  @Implementation(minSdk = N_MR1)
+  protected static void nSetPropertyHolderData(long nativePtr, int[] data, int length) {
+    AnimatedVectorDrawableNatives.nSetPropertyHolderData(nativePtr, data, length);
+  }
+
+  @Implementation(minSdk = N)
+  protected static void nStart(long animatorSetPtr, VectorDrawableAnimatorRT set, int id) {
+    AnimatedVectorDrawableNatives.nStart(animatorSetPtr, set, id);
+  }
+
+  @Implementation(minSdk = N)
+  protected static void nReverse(long animatorSetPtr, VectorDrawableAnimatorRT set, int id) {
+    AnimatedVectorDrawableNatives.nReverse(animatorSetPtr, set, id);
+  }
+
+  @Implementation(minSdk = N)
+  protected static long nCreateGroupPropertyHolder(
+      long nativePtr, int propertyId, float startValue, float endValue) {
+    return AnimatedVectorDrawableNatives.nCreateGroupPropertyHolder(
+        nativePtr, propertyId, startValue, endValue);
+  }
+
+  @Implementation(minSdk = N)
+  protected static long nCreatePathDataPropertyHolder(
+      long nativePtr, long startValuePtr, long endValuePtr) {
+    return AnimatedVectorDrawableNatives.nCreatePathDataPropertyHolder(
+        nativePtr, startValuePtr, endValuePtr);
+  }
+
+  @Implementation(minSdk = N)
+  protected static long nCreatePathColorPropertyHolder(
+      long nativePtr, int propertyId, int startValue, int endValue) {
+    return AnimatedVectorDrawableNatives.nCreatePathColorPropertyHolder(
+        nativePtr, propertyId, startValue, endValue);
+  }
+
+  @Implementation(minSdk = N)
+  protected static long nCreatePathPropertyHolder(
+      long nativePtr, int propertyId, float startValue, float endValue) {
+    return AnimatedVectorDrawableNatives.nCreatePathPropertyHolder(
+        nativePtr, propertyId, startValue, endValue);
+  }
+
+  @Implementation(minSdk = N)
+  protected static long nCreateRootAlphaPropertyHolder(
+      long nativePtr, float startValue, float endValue) {
+    return AnimatedVectorDrawableNatives.nCreateRootAlphaPropertyHolder(
+        nativePtr, startValue, endValue);
+  }
+
+  @Implementation(minSdk = N)
+  protected static void nEnd(long animatorSetPtr) {
+    AnimatedVectorDrawableNatives.nEnd(animatorSetPtr);
+  }
+
+  @Implementation(minSdk = N)
+  protected static void nReset(long animatorSetPtr) {
+    AnimatedVectorDrawableNatives.nReset(animatorSetPtr);
+  }
+
+  /** Shadow picker for {@link AnimatedVectorDrawable}. */
+  public static final class Picker extends GraphicsShadowPicker<Object> {
+    public Picker() {
+      super(null, ShadowNativeAnimatedVectorDrawable.class);
+    }
+  }
+}
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeBaseCanvas.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeBaseCanvas.java
new file mode 100644
index 0000000..4208052
--- /dev/null
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeBaseCanvas.java
@@ -0,0 +1,877 @@
+package org.robolectric.shadows;
+
+import static android.os.Build.VERSION_CODES.O;
+import static android.os.Build.VERSION_CODES.O_MR1;
+import static android.os.Build.VERSION_CODES.P;
+import static android.os.Build.VERSION_CODES.Q;
+import static android.os.Build.VERSION_CODES.S;
+import static android.os.Build.VERSION_CODES.TIRAMISU;
+import static org.robolectric.util.reflector.Reflector.reflector;
+
+import android.annotation.ColorLong;
+import android.graphics.BaseCanvas;
+import android.graphics.Bitmap;
+import android.graphics.Paint;
+import android.graphics.Path;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.annotation.Implementation;
+import org.robolectric.annotation.Implements;
+import org.robolectric.annotation.RealObject;
+import org.robolectric.nativeruntime.BaseCanvasNatives;
+import org.robolectric.shadows.ShadowNativeBaseCanvas.Picker;
+import org.robolectric.util.reflector.Accessor;
+import org.robolectric.util.reflector.ForType;
+
+/** Shadow for {@link BaseCanvas} that is backed by native code */
+@Implements(
+    value = BaseCanvas.class,
+    minSdk = O,
+    shadowPicker = Picker.class,
+    isInAndroidSdk = false)
+public class ShadowNativeBaseCanvas extends ShadowCanvas {
+
+  @RealObject BaseCanvas realBaseCanvas;
+
+  @Implementation(minSdk = Q)
+  protected static void nDrawBitmap(
+      long nativeCanvas,
+      long bitmapHandle,
+      float left,
+      float top,
+      long nativePaintOrZero,
+      int canvasDensity,
+      int screenDensity,
+      int bitmapDensity) {
+    BaseCanvasNatives.nDrawBitmap(
+        nativeCanvas,
+        bitmapHandle,
+        left,
+        top,
+        nativePaintOrZero,
+        canvasDensity,
+        screenDensity,
+        bitmapDensity);
+  }
+
+  @Implementation(minSdk = Q)
+  protected static void nDrawBitmap(
+      long nativeCanvas,
+      long bitmapHandle,
+      float srcLeft,
+      float srcTop,
+      float srcRight,
+      float srcBottom,
+      float dstLeft,
+      float dstTop,
+      float dstRight,
+      float dstBottom,
+      long nativePaintOrZero,
+      int screenDensity,
+      int bitmapDensity) {
+    BaseCanvasNatives.nDrawBitmap(
+        nativeCanvas,
+        bitmapHandle,
+        srcLeft,
+        srcTop,
+        srcRight,
+        srcBottom,
+        dstLeft,
+        dstTop,
+        dstRight,
+        dstBottom,
+        nativePaintOrZero,
+        screenDensity,
+        bitmapDensity);
+  }
+
+  @Implementation(minSdk = O)
+  protected static void nDrawBitmap(
+      long nativeCanvas,
+      int[] colors,
+      int offset,
+      int stride,
+      float x,
+      float y,
+      int width,
+      int height,
+      boolean hasAlpha,
+      long nativePaintOrZero) {
+    BaseCanvasNatives.nDrawBitmap(
+        nativeCanvas, colors, offset, stride, x, y, width, height, hasAlpha, nativePaintOrZero);
+  }
+
+  @Implementation(minSdk = O, maxSdk = P)
+  protected static void nDrawBitmap(
+      long nativeCanvas,
+      Bitmap bitmap,
+      float left,
+      float top,
+      long nativePaintOrZero,
+      int canvasDensity,
+      int screenDensity,
+      int bitmapDensity) {
+    BaseCanvasNatives.nDrawBitmap(
+        nativeCanvas,
+        bitmap.getNativeInstance(),
+        left,
+        top,
+        nativePaintOrZero,
+        canvasDensity,
+        screenDensity,
+        bitmapDensity);
+  }
+
+  @Implementation(minSdk = O, maxSdk = P)
+  protected static void nDrawBitmap(
+      long nativeCanvas,
+      Bitmap bitmap,
+      float srcLeft,
+      float srcTop,
+      float srcRight,
+      float srcBottom,
+      float dstLeft,
+      float dstTop,
+      float dstRight,
+      float dstBottom,
+      long nativePaintOrZero,
+      int screenDensity,
+      int bitmapDensity) {
+    BaseCanvasNatives.nDrawBitmap(
+        nativeCanvas,
+        bitmap.getNativeInstance(),
+        srcLeft,
+        srcTop,
+        srcRight,
+        srcBottom,
+        dstLeft,
+        dstTop,
+        dstRight,
+        dstBottom,
+        nativePaintOrZero,
+        screenDensity,
+        bitmapDensity);
+  }
+
+  @Implementation(minSdk = O)
+  protected static void nDrawColor(long nativeCanvas, int color, int mode) {
+    BaseCanvasNatives.nDrawColor(nativeCanvas, color, mode);
+  }
+
+  @Implementation(minSdk = Q)
+  protected static void nDrawColor(
+      long nativeCanvas, long nativeColorSpace, @ColorLong long color, int mode) {
+    BaseCanvasNatives.nDrawColor(nativeCanvas, nativeColorSpace, color, mode);
+  }
+
+  @Implementation(minSdk = O)
+  protected static void nDrawPaint(long nativeCanvas, long nativePaint) {
+    BaseCanvasNatives.nDrawPaint(nativeCanvas, nativePaint);
+  }
+
+  @Implementation(minSdk = O)
+  protected static void nDrawPoint(long canvasHandle, float x, float y, long paintHandle) {
+    BaseCanvasNatives.nDrawPoint(canvasHandle, x, y, paintHandle);
+  }
+
+  @Implementation(minSdk = O)
+  protected static void nDrawPoints(
+      long canvasHandle, float[] pts, int offset, int count, long paintHandle) {
+    BaseCanvasNatives.nDrawPoints(canvasHandle, pts, offset, count, paintHandle);
+  }
+
+  @Implementation(minSdk = O)
+  protected static void nDrawLine(
+      long nativeCanvas, float startX, float startY, float stopX, float stopY, long nativePaint) {
+    BaseCanvasNatives.nDrawLine(nativeCanvas, startX, startY, stopX, stopY, nativePaint);
+  }
+
+  @Implementation(minSdk = O)
+  protected static void nDrawLines(
+      long canvasHandle, float[] pts, int offset, int count, long paintHandle) {
+    BaseCanvasNatives.nDrawLines(canvasHandle, pts, offset, count, paintHandle);
+  }
+
+  @Implementation(minSdk = O)
+  protected static void nDrawRect(
+      long nativeCanvas, float left, float top, float right, float bottom, long nativePaint) {
+    BaseCanvasNatives.nDrawRect(nativeCanvas, left, top, right, bottom, nativePaint);
+  }
+
+  @Implementation(minSdk = O)
+  protected static void nDrawOval(
+      long nativeCanvas, float left, float top, float right, float bottom, long nativePaint) {
+    BaseCanvasNatives.nDrawOval(nativeCanvas, left, top, right, bottom, nativePaint);
+  }
+
+  @Implementation(minSdk = O)
+  protected static void nDrawCircle(
+      long nativeCanvas, float cx, float cy, float radius, long nativePaint) {
+    BaseCanvasNatives.nDrawCircle(nativeCanvas, cx, cy, radius, nativePaint);
+  }
+
+  @Implementation(minSdk = O)
+  protected static void nDrawArc(
+      long nativeCanvas,
+      float left,
+      float top,
+      float right,
+      float bottom,
+      float startAngle,
+      float sweep,
+      boolean useCenter,
+      long nativePaint) {
+    BaseCanvasNatives.nDrawArc(
+        nativeCanvas, left, top, right, bottom, startAngle, sweep, useCenter, nativePaint);
+  }
+
+  @Implementation(minSdk = O)
+  protected static void nDrawRoundRect(
+      long nativeCanvas,
+      float left,
+      float top,
+      float right,
+      float bottom,
+      float rx,
+      float ry,
+      long nativePaint) {
+    BaseCanvasNatives.nDrawRoundRect(nativeCanvas, left, top, right, bottom, rx, ry, nativePaint);
+  }
+
+  @Implementation(minSdk = Q)
+  protected static void nDrawDoubleRoundRect(
+      long nativeCanvas,
+      float outerLeft,
+      float outerTop,
+      float outerRight,
+      float outerBottom,
+      float outerRx,
+      float outerRy,
+      float innerLeft,
+      float innerTop,
+      float innerRight,
+      float innerBottom,
+      float innerRx,
+      float innerRy,
+      long nativePaint) {
+    BaseCanvasNatives.nDrawDoubleRoundRect(
+        nativeCanvas,
+        outerLeft,
+        outerTop,
+        outerRight,
+        outerBottom,
+        outerRx,
+        outerRy,
+        innerLeft,
+        innerTop,
+        innerRight,
+        innerBottom,
+        innerRx,
+        innerRy,
+        nativePaint);
+  }
+
+  @Implementation(minSdk = Q)
+  protected static void nDrawDoubleRoundRect(
+      long nativeCanvas,
+      float outerLeft,
+      float outerTop,
+      float outerRight,
+      float outerBottom,
+      float[] outerRadii,
+      float innerLeft,
+      float innerTop,
+      float innerRight,
+      float innerBottom,
+      float[] innerRadii,
+      long nativePaint) {
+    BaseCanvasNatives.nDrawDoubleRoundRect(
+        nativeCanvas,
+        outerLeft,
+        outerTop,
+        outerRight,
+        outerBottom,
+        outerRadii,
+        innerLeft,
+        innerTop,
+        innerRight,
+        innerBottom,
+        innerRadii,
+        nativePaint);
+  }
+
+  @Implementation(minSdk = O)
+  protected static void nDrawPath(long nativeCanvas, long nativePath, long nativePaint) {
+    BaseCanvasNatives.nDrawPath(nativeCanvas, nativePath, nativePaint);
+  }
+
+  @Implementation(minSdk = O)
+  protected static void nDrawRegion(long nativeCanvas, long nativeRegion, long nativePaint) {
+    BaseCanvasNatives.nDrawRegion(nativeCanvas, nativeRegion, nativePaint);
+  }
+
+  @Implementation(minSdk = O)
+  protected static void nDrawNinePatch(
+      long nativeCanvas,
+      long nativeBitmap,
+      long ninePatch,
+      float dstLeft,
+      float dstTop,
+      float dstRight,
+      float dstBottom,
+      long nativePaintOrZero,
+      int screenDensity,
+      int bitmapDensity) {
+    BaseCanvasNatives.nDrawNinePatch(
+        nativeCanvas,
+        nativeBitmap,
+        ninePatch,
+        dstLeft,
+        dstTop,
+        dstRight,
+        dstBottom,
+        nativePaintOrZero,
+        screenDensity,
+        bitmapDensity);
+  }
+
+  @Implementation(minSdk = Q)
+  protected static void nDrawBitmapMatrix(
+      long nativeCanvas, long bitmapHandle, long nativeMatrix, long nativePaint) {
+    BaseCanvasNatives.nDrawBitmapMatrix(nativeCanvas, bitmapHandle, nativeMatrix, nativePaint);
+  }
+
+  @Implementation(minSdk = O, maxSdk = P)
+  protected static void nDrawBitmapMatrix(
+      long nativeCanvas, Bitmap bitmap, long nativeMatrix, long nativePaint) {
+    BaseCanvasNatives.nDrawBitmapMatrix(
+        nativeCanvas, bitmap.getNativeInstance(), nativeMatrix, nativePaint);
+  }
+
+  @Implementation(minSdk = Q)
+  protected static void nDrawBitmapMesh(
+      long nativeCanvas,
+      long bitmapHandle,
+      int meshWidth,
+      int meshHeight,
+      float[] verts,
+      int vertOffset,
+      int[] colors,
+      int colorOffset,
+      long nativePaint) {
+    BaseCanvasNatives.nDrawBitmapMesh(
+        nativeCanvas,
+        bitmapHandle,
+        meshWidth,
+        meshHeight,
+        verts,
+        vertOffset,
+        colors,
+        colorOffset,
+        nativePaint);
+  }
+
+  @Implementation(minSdk = O, maxSdk = P)
+  protected static void nDrawBitmapMesh(
+      long nativeCanvas,
+      Bitmap bitmap,
+      int meshWidth,
+      int meshHeight,
+      float[] verts,
+      int vertOffset,
+      int[] colors,
+      int colorOffset,
+      long nativePaint) {
+    BaseCanvasNatives.nDrawBitmapMesh(
+        nativeCanvas,
+        bitmap.getNativeInstance(),
+        meshWidth,
+        meshHeight,
+        verts,
+        vertOffset,
+        colors,
+        colorOffset,
+        nativePaint);
+  }
+
+  @Implementation(minSdk = O)
+  protected static void nDrawVertices(
+      long nativeCanvas,
+      int mode,
+      int n,
+      float[] verts,
+      int vertOffset,
+      float[] texs,
+      int texOffset,
+      int[] colors,
+      int colorOffset,
+      short[] indices,
+      int indexOffset,
+      int indexCount,
+      long nativePaint) {
+    BaseCanvasNatives.nDrawVertices(
+        nativeCanvas,
+        mode,
+        n,
+        verts,
+        vertOffset,
+        texs,
+        texOffset,
+        colors,
+        colorOffset,
+        indices,
+        indexOffset,
+        indexCount,
+        nativePaint);
+  }
+
+  @Implementation(minSdk = S)
+  protected static void nDrawGlyphs(
+      long nativeCanvas,
+      int[] glyphIds,
+      float[] positions,
+      int glyphIdStart,
+      int positionStart,
+      int glyphCount,
+      long nativeFont,
+      long nativePaint) {
+    BaseCanvasNatives.nDrawGlyphs(
+        nativeCanvas,
+        glyphIds,
+        positions,
+        glyphIdStart,
+        positionStart,
+        glyphCount,
+        nativeFont,
+        nativePaint);
+  }
+
+  @Implementation(minSdk = P)
+  protected static void nDrawText(
+      long nativeCanvas,
+      char[] text,
+      int index,
+      int count,
+      float x,
+      float y,
+      int flags,
+      long nativePaint) {
+    // This native code calls Typeface::resolveDefault, which requires Typeface clinit to have run.
+    ShadowNativeTypeface.ensureInitialized();
+    BaseCanvasNatives.nDrawText(nativeCanvas, text, index, count, x, y, flags, nativePaint);
+  }
+
+  @Implementation(minSdk = P)
+  protected static void nDrawText(
+      long nativeCanvas,
+      String text,
+      int start,
+      int end,
+      float x,
+      float y,
+      int flags,
+      long nativePaint) {
+    // This native code calls Typeface::resolveDefault, which requires Typeface clinit to have run.
+    ShadowNativeTypeface.ensureInitialized();
+    BaseCanvasNatives.nDrawText(nativeCanvas, text, start, end, x, y, flags, nativePaint);
+  }
+
+  @Implementation(minSdk = O, maxSdk = O_MR1)
+  protected static void nDrawText(
+      long nativeCanvas,
+      char[] text,
+      int index,
+      int count,
+      float x,
+      float y,
+      int flags,
+      long nativePaint,
+      long nativeTypeface) {
+    // This native code calls Typeface::resolveDefault, which requires Typeface clinit to have run.
+    ShadowNativeTypeface.ensureInitialized();
+    BaseCanvasNatives.nDrawText(
+        nativeCanvas, text, index, count, x, y, flags, nativePaint, nativeTypeface);
+  }
+
+  @Implementation(minSdk = O, maxSdk = O_MR1)
+  protected static void nDrawText(
+      long nativeCanvas,
+      String text,
+      int start,
+      int end,
+      float x,
+      float y,
+      int flags,
+      long nativePaint,
+      long nativeTypeface) {
+    // This native code calls Typeface::resolveDefault, which requires Typeface clinit to have run.
+    ShadowNativeTypeface.ensureInitialized();
+    BaseCanvasNatives.nDrawText(
+        nativeCanvas, text, start, end, x, y, flags, nativePaint, nativeTypeface);
+  }
+
+  @Implementation(minSdk = P)
+  protected static void nDrawTextRun(
+      long nativeCanvas,
+      String text,
+      int start,
+      int end,
+      int contextStart,
+      int contextEnd,
+      float x,
+      float y,
+      boolean isRtl,
+      long nativePaint) {
+    // This native code calls Typeface::resolveDefault, which requires Typeface clinit to have run.
+    ShadowNativeTypeface.ensureInitialized();
+    BaseCanvasNatives.nDrawTextRun(
+        nativeCanvas, text, start, end, contextStart, contextEnd, x, y, isRtl, nativePaint);
+  }
+
+  /**
+   * The signature of this method is the same from SDK levels O and above, but the last native
+   * pointer changed from a Typeface pointer to a MeasuredParagraph pointer in P.
+   */
+  @Implementation(minSdk = O)
+  protected static void nDrawTextRun(
+      long nativeCanvas,
+      char[] text,
+      int start,
+      int count,
+      int contextStart,
+      int contextCount,
+      float x,
+      float y,
+      boolean isRtl,
+      long nativePaint,
+      long nativeTypefaceOrPrecomputedText) {
+    // This native code calls Typeface::resolveDefault, which requires Typeface clinit to have run.
+    ShadowNativeTypeface.ensureInitialized();
+    if (RuntimeEnvironment.getApiLevel() >= P) {
+      BaseCanvasNatives.nDrawTextRun(
+          nativeCanvas,
+          text,
+          start,
+          count,
+          contextStart,
+          contextCount,
+          x,
+          y,
+          isRtl,
+          nativePaint,
+          nativeTypefaceOrPrecomputedText);
+    } else {
+      BaseCanvasNatives.nDrawTextRunTypeface(
+          nativeCanvas,
+          text,
+          start,
+          count,
+          contextStart,
+          contextCount,
+          x,
+          y,
+          isRtl,
+          nativePaint,
+          nativeTypefaceOrPrecomputedText);
+    }
+  }
+
+  @Implementation(minSdk = O, maxSdk = O_MR1)
+  protected static void nDrawTextRun(
+      long nativeCanvas,
+      String text,
+      int start,
+      int end,
+      int contextStart,
+      int contextEnd,
+      float x,
+      float y,
+      boolean isRtl,
+      long nativePaint,
+      long nativeTypeface) {
+    // This native code calls Typeface::resolveDefault, which requires Typeface clinit to have run.
+    ShadowNativeTypeface.ensureInitialized();
+    BaseCanvasNatives.nDrawTextRun(
+        nativeCanvas,
+        text,
+        start,
+        end,
+        contextStart,
+        contextEnd,
+        x,
+        y,
+        isRtl,
+        nativePaint,
+        nativeTypeface);
+  }
+
+  @Implementation(minSdk = P)
+  protected static void nDrawTextOnPath(
+      long nativeCanvas,
+      char[] text,
+      int index,
+      int count,
+      long nativePath,
+      float hOffset,
+      float vOffset,
+      int bidiFlags,
+      long nativePaint) {
+    // This native code calls Typeface::resolveDefault, which requires Typeface clinit to have run.
+    ShadowNativeTypeface.ensureInitialized();
+    BaseCanvasNatives.nDrawTextOnPath(
+        nativeCanvas, text, index, count, nativePath, hOffset, vOffset, bidiFlags, nativePaint);
+  }
+
+  @Implementation(minSdk = P)
+  protected static void nDrawTextOnPath(
+      long nativeCanvas,
+      String text,
+      long nativePath,
+      float hOffset,
+      float vOffset,
+      int flags,
+      long nativePaint) {
+    // This native code calls Typeface::resolveDefault, which requires Typeface clinit to have run.
+    ShadowNativeTypeface.ensureInitialized();
+    BaseCanvasNatives.nDrawTextOnPath(
+        nativeCanvas, text, nativePath, hOffset, vOffset, flags, nativePaint);
+  }
+
+  @Implementation(minSdk = O, maxSdk = O_MR1)
+  protected static void nDrawTextOnPath(
+      long nativeCanvas,
+      char[] text,
+      int index,
+      int count,
+      long nativePath,
+      float hOffset,
+      float vOffset,
+      int bidiFlags,
+      long nativePaint,
+      long nativeTypeface) {
+    // This native code calls Typeface::resolveDefault, which requires Typeface clinit to have run.
+    ShadowNativeTypeface.ensureInitialized();
+    BaseCanvasNatives.nDrawTextOnPath(
+        nativeCanvas,
+        text,
+        index,
+        count,
+        nativePath,
+        hOffset,
+        vOffset,
+        bidiFlags,
+        nativePaint,
+        nativeTypeface);
+  }
+
+  @Implementation(minSdk = O, maxSdk = O_MR1)
+  protected static void nDrawTextOnPath(
+      long nativeCanvas,
+      String text,
+      long nativePath,
+      float hOffset,
+      float vOffset,
+      int flags,
+      long nativePaint,
+      long nativeTypeface) {
+    // This native code calls Typeface::resolveDefault, which requires Typeface clinit to have run.
+    ShadowNativeTypeface.ensureInitialized();
+    BaseCanvasNatives.nDrawTextOnPath(
+        nativeCanvas, text, nativePath, hOffset, vOffset, flags, nativePaint, nativeTypeface);
+  }
+
+  @Implementation(minSdk = S, maxSdk = TIRAMISU)
+  protected static void nPunchHole(
+      long renderer, float left, float top, float right, float bottom, float rx, float ry) {
+    BaseCanvasNatives.nPunchHole(renderer, left, top, right, bottom, rx, ry);
+  }
+
+  @Implementation(minSdk = 10000)
+  protected static void nPunchHole(
+      long renderer,
+      float left,
+      float top,
+      float right,
+      float bottom,
+      float rx,
+      float ry,
+      float alpha) {
+    nPunchHole(renderer, left, top, right, bottom, rx, ry);
+  }
+
+  long getNativeCanvas() {
+    return reflector(BaseCanvasReflector.class, realBaseCanvas).getNativeCanvas();
+  }
+
+  @Override
+  public void appendDescription(String s) {
+    throw new UnsupportedOperationException(
+        "Legacy ShadowCanvas description APIs are not supported");
+  }
+
+  @Override
+  public String getDescription() {
+    throw new UnsupportedOperationException(
+        "Legacy ShadowCanvas description APIs are not supported");
+  }
+
+  @Override
+  public int getPathPaintHistoryCount() {
+    throw new UnsupportedOperationException(
+        "Legacy ShadowCanvas description APIs are not supported");
+  }
+
+  @Override
+  public int getCirclePaintHistoryCount() {
+    throw new UnsupportedOperationException(
+        "Legacy ShadowCanvas description APIs are not supported");
+  }
+
+  @Override
+  public int getArcPaintHistoryCount() {
+    throw new UnsupportedOperationException(
+        "Legacy ShadowCanvas description APIs are not supported");
+  }
+
+  @Override
+  public boolean hasDrawnPath() {
+    throw new UnsupportedOperationException(
+        "Legacy ShadowCanvas description APIs are not supported");
+  }
+
+  @Override
+  public boolean hasDrawnCircle() {
+    throw new UnsupportedOperationException(
+        "Legacy ShadowCanvas description APIs are not supported");
+  }
+
+  @Override
+  public Paint getDrawnPathPaint(int i) {
+    throw new UnsupportedOperationException(
+        "Legacy ShadowCanvas description APIs are not supported");
+  }
+
+  @Override
+  public Path getDrawnPath(int i) {
+    throw new UnsupportedOperationException(
+        "Legacy ShadowCanvas description APIs are not supported");
+  }
+
+  @Override
+  public CirclePaintHistoryEvent getDrawnCircle(int i) {
+    throw new UnsupportedOperationException(
+        "Legacy ShadowCanvas description APIs are not supported");
+  }
+
+  @Override
+  public ArcPaintHistoryEvent getDrawnArc(int i) {
+    throw new UnsupportedOperationException(
+        "Legacy ShadowCanvas description APIs are not supported");
+  }
+
+  @Override
+  public void resetCanvasHistory() {
+    throw new UnsupportedOperationException(
+        "Legacy ShadowCanvas description APIs are not supported");
+  }
+
+  @Override
+  public Paint getDrawnPaint() {
+    throw new UnsupportedOperationException(
+        "Legacy ShadowCanvas description APIs are not supported");
+  }
+
+  @Override
+  public void setHeight(int height) {
+    throw new UnsupportedOperationException("setHeight is not supported in native Canvas");
+  }
+
+  @Override
+  public void setWidth(int width) {
+    throw new UnsupportedOperationException("setWidth is not supported in native Canvas");
+  }
+
+  @Override
+  public TextHistoryEvent getDrawnTextEvent(int i) {
+    throw new UnsupportedOperationException(
+        "Legacy ShadowCanvas description APIs are not supported");
+  }
+
+  @Override
+  public int getTextHistoryCount() {
+    throw new UnsupportedOperationException(
+        "Legacy ShadowCanvas description APIs are not supported");
+  }
+
+  @Override
+  public RectPaintHistoryEvent getDrawnRect(int i) {
+    throw new UnsupportedOperationException(
+        "Legacy ShadowCanvas description APIs are not supported");
+  }
+
+  @Override
+  public RectPaintHistoryEvent getLastDrawnRect() {
+    throw new UnsupportedOperationException(
+        "Legacy ShadowCanvas description APIs are not supported");
+  }
+
+  @Override
+  public int getRectPaintHistoryCount() {
+    throw new UnsupportedOperationException(
+        "Legacy ShadowCanvas description APIs are not supported");
+  }
+
+  @Override
+  public RoundRectPaintHistoryEvent getDrawnRoundRect(int i) {
+    throw new UnsupportedOperationException(
+        "Legacy ShadowCanvas description APIs are not supported");
+  }
+
+  @Override
+  public RoundRectPaintHistoryEvent getLastDrawnRoundRect() {
+    throw new UnsupportedOperationException(
+        "Legacy ShadowCanvas description APIs are not supported");
+  }
+
+  @Override
+  public int getRoundRectPaintHistoryCount() {
+    throw new UnsupportedOperationException(
+        "Legacy ShadowCanvas description APIs are not supported");
+  }
+
+  @Override
+  public LinePaintHistoryEvent getDrawnLine(int i) {
+    throw new UnsupportedOperationException(
+        "Legacy ShadowCanvas description APIs are not supported");
+  }
+
+  @Override
+  public int getLinePaintHistoryCount() {
+    throw new UnsupportedOperationException(
+        "Legacy ShadowCanvas description APIs are not supported");
+  }
+
+  @Override
+  public int getOvalPaintHistoryCount() {
+    throw new UnsupportedOperationException(
+        "Legacy ShadowCanvas description APIs are not supported");
+  }
+
+  @Override
+  public OvalPaintHistoryEvent getDrawnOval(int i) {
+    throw new UnsupportedOperationException(
+        "Legacy ShadowCanvas description APIs are not supported");
+  }
+
+  @ForType(BaseCanvas.class)
+  interface BaseCanvasReflector {
+    @Accessor("mNativeCanvasWrapper")
+    long getNativeCanvas();
+  }
+
+  /** Shadow picker for {@link BaseCanvas}. */
+  public static final class Picker extends GraphicsShadowPicker<Object> {
+    public Picker() {
+      super(null, ShadowNativeBaseCanvas.class);
+    }
+  }
+}
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeBaseRecordingCanvas.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeBaseRecordingCanvas.java
new file mode 100644
index 0000000..1f061b5
--- /dev/null
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeBaseRecordingCanvas.java
@@ -0,0 +1,597 @@
+package org.robolectric.shadows;
+
+import static android.os.Build.VERSION_CODES.O;
+import static android.os.Build.VERSION_CODES.O_MR1;
+import static android.os.Build.VERSION_CODES.P;
+import static android.os.Build.VERSION_CODES.Q;
+import static android.os.Build.VERSION_CODES.S;
+import static android.os.Build.VERSION_CODES.TIRAMISU;
+
+import android.annotation.ColorLong;
+import android.graphics.BaseRecordingCanvas;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.annotation.Implementation;
+import org.robolectric.annotation.Implements;
+import org.robolectric.nativeruntime.BaseRecordingCanvasNatives;
+import org.robolectric.shadows.ShadowNativeBaseRecordingCanvas.Picker;
+
+/** Shadow for {@link BaseRecordingCanvas} that is backed by native code */
+@Implements(
+    value = BaseRecordingCanvas.class,
+    minSdk = Q,
+    shadowPicker = Picker.class,
+    isInAndroidSdk = false)
+public class ShadowNativeBaseRecordingCanvas extends ShadowNativeCanvas {
+
+  @Implementation
+  protected static void nDrawBitmap(
+      long nativeCanvas,
+      long bitmapHandle,
+      float left,
+      float top,
+      long nativePaintOrZero,
+      int canvasDensity,
+      int screenDensity,
+      int bitmapDensity) {
+    BaseRecordingCanvasNatives.nDrawBitmap(
+        nativeCanvas,
+        bitmapHandle,
+        left,
+        top,
+        nativePaintOrZero,
+        canvasDensity,
+        screenDensity,
+        bitmapDensity);
+  }
+
+  @Implementation
+  protected static void nDrawBitmap(
+      long nativeCanvas,
+      long bitmapHandle,
+      float srcLeft,
+      float srcTop,
+      float srcRight,
+      float srcBottom,
+      float dstLeft,
+      float dstTop,
+      float dstRight,
+      float dstBottom,
+      long nativePaintOrZero,
+      int screenDensity,
+      int bitmapDensity) {
+    BaseRecordingCanvasNatives.nDrawBitmap(
+        nativeCanvas,
+        bitmapHandle,
+        srcLeft,
+        srcTop,
+        srcRight,
+        srcBottom,
+        dstLeft,
+        dstTop,
+        dstRight,
+        dstBottom,
+        nativePaintOrZero,
+        screenDensity,
+        bitmapDensity);
+  }
+
+  @Implementation
+  protected static void nDrawBitmap(
+      long nativeCanvas,
+      int[] colors,
+      int offset,
+      int stride,
+      float x,
+      float y,
+      int width,
+      int height,
+      boolean hasAlpha,
+      long nativePaintOrZero) {
+    BaseRecordingCanvasNatives.nDrawBitmap(
+        nativeCanvas, colors, offset, stride, x, y, width, height, hasAlpha, nativePaintOrZero);
+  }
+
+  @Implementation
+  protected static void nDrawColor(long nativeCanvas, int color, int mode) {
+    BaseRecordingCanvasNatives.nDrawColor(nativeCanvas, color, mode);
+  }
+
+  @Implementation
+  protected static void nDrawColor(
+      long nativeCanvas, long nativeColorSpace, @ColorLong long color, int mode) {
+    BaseRecordingCanvasNatives.nDrawColor(nativeCanvas, nativeColorSpace, color, mode);
+  }
+
+  @Implementation
+  protected static void nDrawPaint(long nativeCanvas, long nativePaint) {
+    BaseRecordingCanvasNatives.nDrawPaint(nativeCanvas, nativePaint);
+  }
+
+  @Implementation
+  protected static void nDrawPoint(long canvasHandle, float x, float y, long paintHandle) {
+    BaseRecordingCanvasNatives.nDrawPoint(canvasHandle, x, y, paintHandle);
+  }
+
+  @Implementation
+  protected static void nDrawPoints(
+      long canvasHandle, float[] pts, int offset, int count, long paintHandle) {
+    BaseRecordingCanvasNatives.nDrawPoints(canvasHandle, pts, offset, count, paintHandle);
+  }
+
+  @Implementation
+  protected static void nDrawLine(
+      long nativeCanvas, float startX, float startY, float stopX, float stopY, long nativePaint) {
+    BaseRecordingCanvasNatives.nDrawLine(nativeCanvas, startX, startY, stopX, stopY, nativePaint);
+  }
+
+  @Implementation
+  protected static void nDrawLines(
+      long canvasHandle, float[] pts, int offset, int count, long paintHandle) {
+    BaseRecordingCanvasNatives.nDrawLines(canvasHandle, pts, offset, count, paintHandle);
+  }
+
+  @Implementation
+  protected static void nDrawRect(
+      long nativeCanvas, float left, float top, float right, float bottom, long nativePaint) {
+    BaseRecordingCanvasNatives.nDrawRect(nativeCanvas, left, top, right, bottom, nativePaint);
+  }
+
+  @Implementation
+  protected static void nDrawOval(
+      long nativeCanvas, float left, float top, float right, float bottom, long nativePaint) {
+    BaseRecordingCanvasNatives.nDrawOval(nativeCanvas, left, top, right, bottom, nativePaint);
+  }
+
+  @Implementation
+  protected static void nDrawCircle(
+      long nativeCanvas, float cx, float cy, float radius, long nativePaint) {
+    BaseRecordingCanvasNatives.nDrawCircle(nativeCanvas, cx, cy, radius, nativePaint);
+  }
+
+  @Implementation
+  protected static void nDrawArc(
+      long nativeCanvas,
+      float left,
+      float top,
+      float right,
+      float bottom,
+      float startAngle,
+      float sweep,
+      boolean useCenter,
+      long nativePaint) {
+    BaseRecordingCanvasNatives.nDrawArc(
+        nativeCanvas, left, top, right, bottom, startAngle, sweep, useCenter, nativePaint);
+  }
+
+  @Implementation
+  protected static void nDrawRoundRect(
+      long nativeCanvas,
+      float left,
+      float top,
+      float right,
+      float bottom,
+      float rx,
+      float ry,
+      long nativePaint) {
+    BaseRecordingCanvasNatives.nDrawRoundRect(
+        nativeCanvas, left, top, right, bottom, rx, ry, nativePaint);
+  }
+
+  @Implementation
+  protected static void nDrawDoubleRoundRect(
+      long nativeCanvas,
+      float outerLeft,
+      float outerTop,
+      float outerRight,
+      float outerBottom,
+      float outerRx,
+      float outerRy,
+      float innerLeft,
+      float innerTop,
+      float innerRight,
+      float innerBottom,
+      float innerRx,
+      float innerRy,
+      long nativePaint) {
+    BaseRecordingCanvasNatives.nDrawDoubleRoundRect(
+        nativeCanvas,
+        outerLeft,
+        outerTop,
+        outerRight,
+        outerBottom,
+        outerRx,
+        outerRy,
+        innerLeft,
+        innerTop,
+        innerRight,
+        innerBottom,
+        innerRx,
+        innerRy,
+        nativePaint);
+  }
+
+  @Implementation
+  protected static void nDrawDoubleRoundRect(
+      long nativeCanvas,
+      float outerLeft,
+      float outerTop,
+      float outerRight,
+      float outerBottom,
+      float[] outerRadii,
+      float innerLeft,
+      float innerTop,
+      float innerRight,
+      float innerBottom,
+      float[] innerRadii,
+      long nativePaint) {
+    BaseRecordingCanvasNatives.nDrawDoubleRoundRect(
+        nativeCanvas,
+        outerLeft,
+        outerTop,
+        outerRight,
+        outerBottom,
+        outerRadii,
+        innerLeft,
+        innerTop,
+        innerRight,
+        innerBottom,
+        innerRadii,
+        nativePaint);
+  }
+
+  @Implementation
+  protected static void nDrawPath(long nativeCanvas, long nativePath, long nativePaint) {
+    BaseRecordingCanvasNatives.nDrawPath(nativeCanvas, nativePath, nativePaint);
+  }
+
+  @Implementation
+  protected static void nDrawRegion(long nativeCanvas, long nativeRegion, long nativePaint) {
+    BaseRecordingCanvasNatives.nDrawRegion(nativeCanvas, nativeRegion, nativePaint);
+  }
+
+  @Implementation
+  protected static void nDrawNinePatch(
+      long nativeCanvas,
+      long nativeBitmap,
+      long ninePatch,
+      float dstLeft,
+      float dstTop,
+      float dstRight,
+      float dstBottom,
+      long nativePaintOrZero,
+      int screenDensity,
+      int bitmapDensity) {
+    BaseRecordingCanvasNatives.nDrawNinePatch(
+        nativeCanvas,
+        nativeBitmap,
+        ninePatch,
+        dstLeft,
+        dstTop,
+        dstRight,
+        dstBottom,
+        nativePaintOrZero,
+        screenDensity,
+        bitmapDensity);
+  }
+
+  @Implementation
+  protected static void nDrawBitmapMatrix(
+      long nativeCanvas, long bitmapHandle, long nativeMatrix, long nativePaint) {
+    BaseRecordingCanvasNatives.nDrawBitmapMatrix(
+        nativeCanvas, bitmapHandle, nativeMatrix, nativePaint);
+  }
+
+  @Implementation
+  protected static void nDrawBitmapMesh(
+      long nativeCanvas,
+      long bitmapHandle,
+      int meshWidth,
+      int meshHeight,
+      float[] verts,
+      int vertOffset,
+      int[] colors,
+      int colorOffset,
+      long nativePaint) {
+    BaseRecordingCanvasNatives.nDrawBitmapMesh(
+        nativeCanvas,
+        bitmapHandle,
+        meshWidth,
+        meshHeight,
+        verts,
+        vertOffset,
+        colors,
+        colorOffset,
+        nativePaint);
+  }
+
+  @Implementation
+  protected static void nDrawVertices(
+      long nativeCanvas,
+      int mode,
+      int n,
+      float[] verts,
+      int vertOffset,
+      float[] texs,
+      int texOffset,
+      int[] colors,
+      int colorOffset,
+      short[] indices,
+      int indexOffset,
+      int indexCount,
+      long nativePaint) {
+    BaseRecordingCanvasNatives.nDrawVertices(
+        nativeCanvas,
+        mode,
+        n,
+        verts,
+        vertOffset,
+        texs,
+        texOffset,
+        colors,
+        colorOffset,
+        indices,
+        indexOffset,
+        indexCount,
+        nativePaint);
+  }
+
+  @Implementation(minSdk = S)
+  protected static void nDrawGlyphs(
+      long nativeCanvas,
+      int[] glyphIds,
+      float[] positions,
+      int glyphIdStart,
+      int positionStart,
+      int glyphCount,
+      long nativeFont,
+      long nativePaint) {
+    BaseRecordingCanvasNatives.nDrawGlyphs(
+        nativeCanvas,
+        glyphIds,
+        positions,
+        glyphIdStart,
+        positionStart,
+        glyphCount,
+        nativeFont,
+        nativePaint);
+  }
+
+  @Implementation
+  protected static void nDrawText(
+      long nativeCanvas,
+      char[] text,
+      int index,
+      int count,
+      float x,
+      float y,
+      int flags,
+      long nativePaint) {
+    BaseRecordingCanvasNatives.nDrawText(
+        nativeCanvas, text, index, count, x, y, flags, nativePaint);
+  }
+
+  @Implementation
+  protected static void nDrawText(
+      long nativeCanvas,
+      String text,
+      int start,
+      int end,
+      float x,
+      float y,
+      int flags,
+      long nativePaint) {
+    BaseRecordingCanvasNatives.nDrawText(nativeCanvas, text, start, end, x, y, flags, nativePaint);
+  }
+
+  @Implementation(minSdk = O, maxSdk = O_MR1)
+  protected static void nDrawText(
+      long nativeCanvas,
+      char[] text,
+      int index,
+      int count,
+      float x,
+      float y,
+      int flags,
+      long nativePaint,
+      long nativeTypeface) {
+    BaseRecordingCanvasNatives.nDrawText(
+        nativeCanvas, text, index, count, x, y, flags, nativePaint, nativeTypeface);
+  }
+
+  @Implementation(minSdk = O, maxSdk = O_MR1)
+  protected static void nDrawText(
+      long nativeCanvas,
+      String text,
+      int start,
+      int end,
+      float x,
+      float y,
+      int flags,
+      long nativePaint,
+      long nativeTypeface) {
+    BaseRecordingCanvasNatives.nDrawText(
+        nativeCanvas, text, start, end, x, y, flags, nativePaint, nativeTypeface);
+  }
+
+  @Implementation
+  protected static void nDrawTextRun(
+      long nativeCanvas,
+      String text,
+      int start,
+      int end,
+      int contextStart,
+      int contextEnd,
+      float x,
+      float y,
+      boolean isRtl,
+      long nativePaint) {
+    BaseRecordingCanvasNatives.nDrawTextRun(
+        nativeCanvas, text, start, end, contextStart, contextEnd, x, y, isRtl, nativePaint);
+  }
+
+  /**
+   * The signature of this method is the same from SDK levels O and above, but the last native
+   * pointer changed from a Typeface pointer to a MeasuredParagraph pointer in P.
+   */
+  @Implementation(minSdk = O)
+  protected static void nDrawTextRun(
+      long nativeCanvas,
+      char[] text,
+      int start,
+      int count,
+      int contextStart,
+      int contextCount,
+      float x,
+      float y,
+      boolean isRtl,
+      long nativePaint,
+      long nativeTypefaceOrPrecomputedText) {
+    if (RuntimeEnvironment.getApiLevel() >= P) {
+      BaseRecordingCanvasNatives.nDrawTextRun(
+          nativeCanvas,
+          text,
+          start,
+          count,
+          contextStart,
+          contextCount,
+          x,
+          y,
+          isRtl,
+          nativePaint,
+          nativeTypefaceOrPrecomputedText);
+    } else {
+      BaseRecordingCanvasNatives.nDrawTextRunTypeface(
+          nativeCanvas,
+          text,
+          start,
+          count,
+          contextStart,
+          contextCount,
+          x,
+          y,
+          isRtl,
+          nativePaint,
+          nativeTypefaceOrPrecomputedText);
+    }
+  }
+
+  @Implementation(minSdk = O, maxSdk = O_MR1)
+  protected static void nDrawTextRun(
+      long nativeCanvas,
+      String text,
+      int start,
+      int end,
+      int contextStart,
+      int contextEnd,
+      float x,
+      float y,
+      boolean isRtl,
+      long nativePaint,
+      long nativeTypeface) {
+    BaseRecordingCanvasNatives.nDrawTextRun(
+        nativeCanvas,
+        text,
+        start,
+        end,
+        contextStart,
+        contextEnd,
+        x,
+        y,
+        isRtl,
+        nativePaint,
+        nativeTypeface);
+  }
+
+  @Implementation
+  protected static void nDrawTextOnPath(
+      long nativeCanvas,
+      char[] text,
+      int index,
+      int count,
+      long nativePath,
+      float hOffset,
+      float vOffset,
+      int bidiFlags,
+      long nativePaint) {
+    BaseRecordingCanvasNatives.nDrawTextOnPath(
+        nativeCanvas, text, index, count, nativePath, hOffset, vOffset, bidiFlags, nativePaint);
+  }
+
+  @Implementation
+  protected static void nDrawTextOnPath(
+      long nativeCanvas,
+      String text,
+      long nativePath,
+      float hOffset,
+      float vOffset,
+      int flags,
+      long nativePaint) {
+    BaseRecordingCanvasNatives.nDrawTextOnPath(
+        nativeCanvas, text, nativePath, hOffset, vOffset, flags, nativePaint);
+  }
+
+  @Implementation(minSdk = O, maxSdk = O_MR1)
+  protected static void nDrawTextOnPath(
+      long nativeCanvas,
+      char[] text,
+      int index,
+      int count,
+      long nativePath,
+      float hOffset,
+      float vOffset,
+      int bidiFlags,
+      long nativePaint,
+      long nativeTypeface) {
+    BaseRecordingCanvasNatives.nDrawTextOnPath(
+        nativeCanvas,
+        text,
+        index,
+        count,
+        nativePath,
+        hOffset,
+        vOffset,
+        bidiFlags,
+        nativePaint,
+        nativeTypeface);
+  }
+
+  @Implementation(minSdk = O, maxSdk = O_MR1)
+  protected static void nDrawTextOnPath(
+      long nativeCanvas,
+      String text,
+      long nativePath,
+      float hOffset,
+      float vOffset,
+      int flags,
+      long nativePaint,
+      long nativeTypeface) {
+    BaseRecordingCanvasNatives.nDrawTextOnPath(
+        nativeCanvas, text, nativePath, hOffset, vOffset, flags, nativePaint, nativeTypeface);
+  }
+
+  @Implementation(minSdk = S, maxSdk = TIRAMISU)
+  protected static void nPunchHole(
+      long renderer, float left, float top, float right, float bottom, float rx, float ry) {
+    BaseRecordingCanvasNatives.nPunchHole(renderer, left, top, right, bottom, rx, ry);
+  }
+
+  @Implementation(minSdk = 10000)
+  protected static void nPunchHole(
+      long renderer,
+      float left,
+      float top,
+      float right,
+      float bottom,
+      float rx,
+      float ry,
+      float alpha) {
+    nPunchHole(renderer, left, top, right, bottom, rx, ry);
+  }
+
+  /** Shadow picker for {@link BaseRecordingCanvas}. */
+  public static final class Picker extends GraphicsShadowPicker<Object> {
+    public Picker() {
+      super(null, ShadowNativeBaseRecordingCanvas.class);
+    }
+  }
+}
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeBitmap.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeBitmap.java
new file mode 100644
index 0000000..cddbd44
--- /dev/null
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeBitmap.java
@@ -0,0 +1,504 @@
+package org.robolectric.shadows;
+
+import static android.os.Build.VERSION_CODES.JELLY_BEAN_MR1;
+import static android.os.Build.VERSION_CODES.LOLLIPOP;
+import static android.os.Build.VERSION_CODES.M;
+import static android.os.Build.VERSION_CODES.N;
+import static android.os.Build.VERSION_CODES.N_MR1;
+import static android.os.Build.VERSION_CODES.O;
+import static android.os.Build.VERSION_CODES.P;
+import static android.os.Build.VERSION_CODES.Q;
+import static android.os.Build.VERSION_CODES.R;
+import static android.os.Build.VERSION_CODES.S;
+import static org.robolectric.util.reflector.Reflector.reflector;
+
+import android.graphics.Bitmap;
+import android.graphics.ColorSpace;
+import android.graphics.ColorSpace.Rgb.TransferParameters;
+import android.graphics.Matrix;
+import android.hardware.HardwareBuffer;
+import android.os.Parcel;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.nio.Buffer;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import org.robolectric.annotation.Implementation;
+import org.robolectric.annotation.Implements;
+import org.robolectric.annotation.RealObject;
+import org.robolectric.annotation.Resetter;
+import org.robolectric.nativeruntime.BitmapNatives;
+import org.robolectric.nativeruntime.ColorSpaceRgbNatives;
+import org.robolectric.nativeruntime.DefaultNativeRuntimeLoader;
+import org.robolectric.nativeruntime.NativeAllocationRegistryNatives;
+import org.robolectric.util.reflector.Accessor;
+import org.robolectric.util.reflector.ForType;
+import org.robolectric.util.reflector.Static;
+
+/** Shadow for {@link Bitmap} that is backed by native code */
+@Implements(value = Bitmap.class, looseSignatures = true, minSdk = O, isInAndroidSdk = false)
+public class ShadowNativeBitmap extends ShadowBitmap {
+
+  @RealObject Bitmap realBitmap;
+
+  private int createdFromResId;
+
+  private static final List<Long> colorSpaceAllocationsP =
+      Collections.synchronizedList(new ArrayList<>());
+
+  /** Called by {@link ShadowNativeBitmapFactory}. */
+  void setCreatedFromResId(int createdFromResId) {
+    this.createdFromResId = createdFromResId;
+  }
+
+  @Implementation(minSdk = Q)
+  protected static Bitmap nativeCreate(
+      int[] colors,
+      int offset,
+      int stride,
+      int width,
+      int height,
+      int nativeConfig,
+      boolean mutable,
+      long nativeColorSpace) {
+    DefaultNativeRuntimeLoader.injectAndLoad();
+    return BitmapNatives.nativeCreate(
+        colors, offset, stride, width, height, nativeConfig, mutable, nativeColorSpace);
+  }
+
+  @Implementation(minSdk = O, maxSdk = P)
+  protected static Bitmap nativeCreate(
+      int[] colors,
+      int offset,
+      int stride,
+      int width,
+      int height,
+      int nativeConfig,
+      boolean mutable,
+      float[] xyzD50,
+      ColorSpace.Rgb.TransferParameters p) {
+    DefaultNativeRuntimeLoader.injectAndLoad();
+    long colorSpacePtr = 0;
+    if (xyzD50 != null && p != null) {
+      colorSpacePtr =
+          ColorSpaceRgbNatives.nativeCreate(
+              (float) p.a,
+              (float) p.b,
+              (float) p.c,
+              (float) p.d,
+              (float) p.e,
+              (float) p.f,
+              (float) p.g,
+              xyzD50);
+      colorSpaceAllocationsP.add(colorSpacePtr);
+    }
+    return nativeCreate(
+        colors, offset, stride, width, height, nativeConfig, mutable, colorSpacePtr);
+  }
+
+  @Implementation(minSdk = LOLLIPOP)
+  protected static Bitmap nativeCopy(long nativeSrcBitmap, int nativeConfig, boolean isMutable) {
+    return BitmapNatives.nativeCopy(nativeSrcBitmap, nativeConfig, isMutable);
+  }
+
+  @Implementation(minSdk = M)
+  protected static Bitmap nativeCopyAshmem(long nativeSrcBitmap) {
+    return BitmapNatives.nativeCopyAshmem(nativeSrcBitmap);
+  }
+
+  @Implementation(minSdk = N)
+  protected static Bitmap nativeCopyAshmemConfig(long nativeSrcBitmap, int nativeConfig) {
+    return BitmapNatives.nativeCopyAshmemConfig(nativeSrcBitmap, nativeConfig);
+  }
+
+  @Implementation(minSdk = N)
+  protected static long nativeGetNativeFinalizer() {
+    return BitmapNatives.nativeGetNativeFinalizer();
+  }
+
+  @Implementation(minSdk = LOLLIPOP)
+  protected static Object nativeRecycle(Object nativeBitmap) {
+    BitmapNatives.nativeRecycle((long) nativeBitmap);
+    return true;
+  }
+
+  @Implementation(minSdk = O)
+  protected static void nativeReconfigure(
+      long nativeBitmap, int width, int height, int config, boolean isPremultiplied) {
+    BitmapNatives.nativeReconfigure(nativeBitmap, width, height, config, isPremultiplied);
+  }
+
+  @Implementation(minSdk = LOLLIPOP)
+  protected static boolean nativeCompress(
+      long nativeBitmap, int format, int quality, OutputStream stream, byte[] tempStorage) {
+    return BitmapNatives.nativeCompress(nativeBitmap, format, quality, stream, tempStorage);
+  }
+
+  @Implementation(minSdk = LOLLIPOP)
+  protected static void nativeErase(long nativeBitmap, int color) {
+    BitmapNatives.nativeErase(nativeBitmap, color);
+  }
+
+  @Implementation(minSdk = Q)
+  protected static void nativeErase(long nativeBitmap, long colorSpacePtr, long color) {
+    BitmapNatives.nativeErase(nativeBitmap, colorSpacePtr, color);
+  }
+
+  @Implementation(minSdk = LOLLIPOP)
+  protected static int nativeRowBytes(long nativeBitmap) {
+    return BitmapNatives.nativeRowBytes(nativeBitmap);
+  }
+
+  @Implementation(minSdk = LOLLIPOP)
+  protected static int nativeConfig(long nativeBitmap) {
+    return BitmapNatives.nativeConfig(nativeBitmap);
+  }
+
+  @Implementation(minSdk = LOLLIPOP)
+  protected static int nativeGetPixel(long nativeBitmap, int x, int y) {
+    return BitmapNatives.nativeGetPixel(nativeBitmap, x, y);
+  }
+
+  @Implementation(minSdk = Q)
+  protected static long nativeGetColor(long nativeBitmap, int x, int y) {
+    return BitmapNatives.nativeGetColor(nativeBitmap, x, y);
+  }
+
+  @Implementation(minSdk = LOLLIPOP)
+  protected static void nativeGetPixels(
+      long nativeBitmap,
+      int[] pixels,
+      int offset,
+      int stride,
+      int x,
+      int y,
+      int width,
+      int height) {
+    BitmapNatives.nativeGetPixels(nativeBitmap, pixels, offset, stride, x, y, width, height);
+  }
+
+  @Implementation(minSdk = LOLLIPOP)
+  protected static void nativeSetPixel(long nativeBitmap, int x, int y, int color) {
+    BitmapNatives.nativeSetPixel(nativeBitmap, x, y, color);
+  }
+
+  @Implementation(minSdk = LOLLIPOP)
+  protected static void nativeSetPixels(
+      long nativeBitmap,
+      int[] colors,
+      int offset,
+      int stride,
+      int x,
+      int y,
+      int width,
+      int height) {
+    BitmapNatives.nativeSetPixels(nativeBitmap, colors, offset, stride, x, y, width, height);
+  }
+
+  @Implementation
+  protected static void nativeCopyPixelsToBuffer(long nativeBitmap, Buffer dst) {
+    BitmapNatives.nativeCopyPixelsToBuffer(nativeBitmap, dst);
+  }
+
+  @Implementation
+  protected static void nativeCopyPixelsFromBuffer(long nativeBitmap, Buffer src) {
+    BitmapNatives.nativeCopyPixelsFromBuffer(nativeBitmap, src);
+  }
+
+  @Implementation
+  protected static int nativeGenerationId(long nativeBitmap) {
+    return BitmapNatives.nativeGenerationId(nativeBitmap);
+  }
+
+  // returns a new bitmap built from the native bitmap's alpha, and the paint
+  @Implementation
+  protected static Bitmap nativeExtractAlpha(long nativeBitmap, long nativePaint, int[] offsetXY) {
+    return BitmapNatives.nativeExtractAlpha(nativeBitmap, nativePaint, offsetXY);
+  }
+
+  @Implementation
+  protected static boolean nativeHasAlpha(long nativeBitmap) {
+    return BitmapNatives.nativeHasAlpha(nativeBitmap);
+  }
+
+  @Implementation(minSdk = LOLLIPOP)
+  protected static boolean nativeIsPremultiplied(long nativeBitmap) {
+    return BitmapNatives.nativeIsPremultiplied(nativeBitmap);
+  }
+
+  @Implementation(minSdk = LOLLIPOP)
+  protected static void nativeSetPremultiplied(long nativeBitmap, boolean isPremul) {
+    BitmapNatives.nativeSetPremultiplied(nativeBitmap, isPremul);
+  }
+
+  @Implementation(minSdk = LOLLIPOP)
+  protected static void nativeSetHasAlpha(
+      long nativeBitmap, boolean hasAlpha, boolean requestPremul) {
+    BitmapNatives.nativeSetHasAlpha(nativeBitmap, hasAlpha, requestPremul);
+  }
+
+  @Implementation(minSdk = JELLY_BEAN_MR1)
+  protected static boolean nativeHasMipMap(long nativeBitmap) {
+    return BitmapNatives.nativeHasMipMap(nativeBitmap);
+  }
+
+  @Implementation(minSdk = JELLY_BEAN_MR1)
+  protected static void nativeSetHasMipMap(long nativeBitmap, boolean hasMipMap) {
+    BitmapNatives.nativeSetHasMipMap(nativeBitmap, hasMipMap);
+  }
+
+  @Implementation
+  protected static boolean nativeSameAs(long nativeBitmap0, long nativeBitmap1) {
+    return BitmapNatives.nativeSameAs(nativeBitmap0, nativeBitmap1);
+  }
+
+  @Implementation(minSdk = N_MR1)
+  protected static void nativePrepareToDraw(long nativeBitmap) {
+    BitmapNatives.nativePrepareToDraw(nativeBitmap);
+  }
+
+  @Implementation(minSdk = O)
+  protected static int nativeGetAllocationByteCount(long nativeBitmap) {
+    return BitmapNatives.nativeGetAllocationByteCount(nativeBitmap);
+  }
+
+  @Implementation(minSdk = O)
+  protected static Bitmap nativeCopyPreserveInternalConfig(long nativeBitmap) {
+    return BitmapNatives.nativeCopyPreserveInternalConfig(nativeBitmap);
+  }
+
+  @Implementation(minSdk = Q)
+  protected static Bitmap nativeWrapHardwareBufferBitmap(
+      HardwareBuffer buffer, long nativeColorSpace) {
+    return BitmapNatives.nativeWrapHardwareBufferBitmap(buffer, nativeColorSpace);
+  }
+
+  @Implementation(minSdk = R)
+  protected static HardwareBuffer nativeGetHardwareBuffer(long nativeBitmap) {
+    return BitmapNatives.nativeGetHardwareBuffer(nativeBitmap);
+  }
+
+  @Implementation(minSdk = O, maxSdk = P)
+  protected static boolean nativeGetColorSpace(long nativePtr, float[] xyz, float[] params) {
+    ColorSpace colorSpace = nativeComputeColorSpace(nativePtr);
+    if (colorSpace == null) {
+      return false;
+    }
+    // In Android P, 'nativeGetColorSpace' is responsible for filling out the 'xyz' and 'params'
+    // float arrays. However, in Q and above, 'nativeGetColorSpace' was removed, and
+    // 'nativeComputeColorSpace' returns the ColorSpace object itself. This means for P, we need to
+    // do the reverse operations and generate the float arrays given the detected color space.
+    if (colorSpace instanceof ColorSpace.Rgb) {
+      TransferParameters transferParameters = ((ColorSpace.Rgb) colorSpace).getTransferParameters();
+      params[0] = (float) transferParameters.a;
+      params[1] = (float) transferParameters.b;
+      params[2] = (float) transferParameters.c;
+      params[3] = (float) transferParameters.d;
+      params[4] = (float) transferParameters.e;
+      params[5] = (float) transferParameters.f;
+      params[6] = (float) transferParameters.g;
+      ColorSpace.Rgb rgb =
+          (ColorSpace.Rgb)
+              ColorSpace.adapt(
+                  colorSpace, reflector(ColorSpaceReflector.class).getIlluminantD50XYZ());
+      rgb.getTransform(xyz);
+    }
+    return true;
+  }
+
+  @Implementation(minSdk = Q)
+  protected static ColorSpace nativeComputeColorSpace(long nativePtr) {
+    return BitmapNatives.nativeComputeColorSpace(nativePtr);
+  }
+
+  @Implementation(minSdk = Q)
+  protected static void nativeSetColorSpace(long nativePtr, long nativeColorSpace) {
+    BitmapNatives.nativeSetColorSpace(nativePtr, nativeColorSpace);
+  }
+
+  @Implementation(minSdk = O)
+  protected static boolean nativeIsSRGB(long nativePtr) {
+    return BitmapNatives.nativeIsSRGB(nativePtr);
+  }
+
+  @Implementation(minSdk = P)
+  protected static boolean nativeIsSRGBLinear(long nativePtr) {
+    return BitmapNatives.nativeIsSRGBLinear(nativePtr);
+  }
+
+  @Implementation(minSdk = Q)
+  protected static void nativeSetImmutable(long nativePtr) {
+    BitmapNatives.nativeSetImmutable(nativePtr);
+  }
+
+  @Implementation(minSdk = Q)
+  protected static boolean nativeIsImmutable(long nativePtr) {
+    return BitmapNatives.nativeIsImmutable(nativePtr);
+  }
+
+  @Implementation(minSdk = S)
+  protected static boolean nativeIsBackedByAshmem(long nativePtr) {
+    return BitmapNatives.nativeIsBackedByAshmem(nativePtr);
+  }
+
+  @ForType(ColorSpace.class)
+  interface ColorSpaceReflector {
+    @Accessor("ILLUMINANT_D50_XYZ")
+    @Static
+    float[] getIlluminantD50XYZ();
+
+    @Accessor("sNamedColorSpaces")
+    ColorSpace[] getNamedColorSpaces();
+  }
+
+  @Implementation
+  protected void writeToParcel(Parcel p, int flags) {
+    // Modeled after
+    // https://cs.android.com/android/platform/superproject/+/android-12.0.0_r1:frameworks/base/libs/hwui/jni/Bitmap.cpp;l=872.
+    reflector(BitmapReflector.class, realBitmap).checkRecycled("Can't parcel a recycled bitmap");
+    int width = realBitmap.getWidth();
+    int height = realBitmap.getHeight();
+    p.writeInt(width);
+    p.writeInt(height);
+    p.writeInt(realBitmap.getDensity());
+    p.writeBoolean(realBitmap.isMutable());
+    p.writeSerializable(realBitmap.getConfig());
+    p.writeString(realBitmap.getColorSpace().getName());
+    p.writeBoolean(realBitmap.hasAlpha());
+    int[] pixels = new int[width * height];
+    realBitmap.getPixels(pixels, 0, width, 0, 0, width, height);
+    p.writeIntArray(pixels);
+  }
+
+  @Implementation
+  protected static Bitmap nativeCreateFromParcel(Parcel p) {
+    int parceledWidth = p.readInt();
+    int parceledHeight = p.readInt();
+    int density = p.readInt();
+    boolean mutable = p.readBoolean();
+    Bitmap.Config parceledConfig = (Bitmap.Config) p.readSerializable();
+    String colorSpaceName = p.readString();
+    boolean hasAlpha = p.readBoolean();
+    ColorSpace colorSpace = null;
+    ColorSpace[] namedColorSpaces = reflector(ColorSpaceReflector.class).getNamedColorSpaces();
+    for (ColorSpace named : namedColorSpaces) {
+      if (named.getName().equals(colorSpaceName)) {
+        colorSpace = named;
+        break;
+      }
+    }
+    int[] parceledColors = new int[parceledHeight * parceledWidth];
+    p.readIntArray(parceledColors);
+    Bitmap bitmap =
+        Bitmap.createBitmap(parceledWidth, parceledHeight, parceledConfig, hasAlpha, colorSpace);
+    bitmap.setPixels(parceledColors, 0, parceledWidth, 0, 0, parceledWidth, parceledHeight);
+    bitmap.setDensity(density);
+    if (!mutable) {
+      bitmap = bitmap.copy(parceledConfig, false);
+    }
+    return bitmap;
+  }
+
+  @ForType(Bitmap.class)
+  interface BitmapReflector {
+    void checkRecycled(String errorMessage);
+  }
+
+  @Override
+  public Bitmap getCreatedFromBitmap() {
+    throw new UnsupportedOperationException("Legacy ShadowBitmap APIs are not supported");
+  }
+
+  /**
+   * Resource ID from which this Bitmap was created.
+   *
+   * @return Resource ID from which this Bitmap was created, or {@code 0} if this Bitmap was not
+   *     created from a resource.
+   */
+  @Override
+  public int getCreatedFromResId() {
+    return createdFromResId;
+  }
+
+  @Override
+  public String getCreatedFromPath() {
+    throw new UnsupportedOperationException("Legacy ShadowBitmap APIs are not supported");
+  }
+
+  @Override
+  public InputStream getCreatedFromStream() {
+    throw new UnsupportedOperationException("Legacy ShadowBitmap APIs are not supported");
+  }
+
+  @Override
+  public byte[] getCreatedFromBytes() {
+    throw new UnsupportedOperationException("Legacy ShadowBitmap APIs are not supported");
+  }
+
+  @Override
+  public int getCreatedFromX() {
+    throw new UnsupportedOperationException("Legacy ShadowBitmap APIs are not supported");
+  }
+
+  @Override
+  public int getCreatedFromY() {
+    throw new UnsupportedOperationException("Legacy ShadowBitmap APIs are not supported");
+  }
+
+  @Override
+  public int getCreatedFromWidth() {
+    throw new UnsupportedOperationException("Legacy ShadowBitmap APIs are not supported");
+  }
+
+  @Override
+  public int getCreatedFromHeight() {
+    throw new UnsupportedOperationException("Legacy ShadowBitmap APIs are not supported");
+  }
+
+  @Override
+  public int[] getCreatedFromColors() {
+    throw new UnsupportedOperationException("Legacy ShadowBitmap APIs are not supported");
+  }
+
+  @Override
+  public Matrix getCreatedFromMatrix() {
+    throw new UnsupportedOperationException("Legacy ShadowBitmap APIs are not supported");
+  }
+
+  @Override
+  public boolean getCreatedFromFilter() {
+    throw new UnsupportedOperationException("Legacy ShadowBitmap APIs are not supported");
+  }
+
+  @Override
+  public void setMutable(boolean mutable) {
+    throw new UnsupportedOperationException("Legacy ShadowBitmap APIs are not supported");
+  }
+
+  @Override
+  public void appendDescription(String s) {
+    throw new UnsupportedOperationException("Legacy ShadowBitmap APIs are not supported");
+  }
+
+  @Override
+  public String getDescription() {
+    throw new UnsupportedOperationException("Legacy ShadowBitmap APIs are not supported");
+  }
+
+  @Override
+  public void setDescription(String s) {
+    throw new UnsupportedOperationException("Legacy ShadowBitmap APIs are not supported");
+  }
+
+  @Resetter
+  public static void reset() {
+    synchronized (colorSpaceAllocationsP) {
+      for (Long ptr : colorSpaceAllocationsP) {
+        NativeAllocationRegistryNatives.applyFreeFunction(
+            ColorSpaceRgbNatives.nativeGetNativeFinalizer(), ptr);
+      }
+      colorSpaceAllocationsP.clear();
+    }
+  }
+}
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeBitmapDrawable.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeBitmapDrawable.java
new file mode 100644
index 0000000..11b0341
--- /dev/null
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeBitmapDrawable.java
@@ -0,0 +1,42 @@
+package org.robolectric.shadows;
+
+import static android.os.Build.VERSION_CODES.O;
+
+import android.graphics.Bitmap;
+import android.graphics.drawable.BitmapDrawable;
+import org.robolectric.annotation.Implements;
+import org.robolectric.annotation.RealObject;
+import org.robolectric.shadow.api.Shadow;
+import org.robolectric.shadows.ShadowNativeBitmapDrawable.Picker;
+
+/** Disable the legacy ShadowBitmapDrawable as it fakes the draw logic. */
+@Implements(
+    value = BitmapDrawable.class,
+    minSdk = O,
+    shadowPicker = Picker.class,
+    isInAndroidSdk = false)
+public class ShadowNativeBitmapDrawable extends ShadowBitmapDrawable {
+  @RealObject BitmapDrawable bitmapDrawable;
+
+  @Override
+  public int getCreatedFromResId() {
+    return ((ShadowNativeBitmap) Shadow.extract(bitmapDrawable.getBitmap())).getCreatedFromResId();
+  }
+
+  @Override
+  protected void setCreatedFromResId(int createdFromResId, String resourceName) {
+    super.setCreatedFromResId(createdFromResId, resourceName);
+    Bitmap bitmap = bitmapDrawable.getBitmap();
+    if (bitmap != null && Shadow.extract(bitmap) instanceof ShadowNativeBitmap) {
+      ShadowNativeBitmap shadowNativeBitmap = Shadow.extract(bitmap);
+      shadowNativeBitmap.setCreatedFromResId(createdFromResId);
+    }
+  }
+
+  /** Shadow picker for {@link BitmapDrawable}. */
+  public static final class Picker extends GraphicsShadowPicker<Object> {
+    public Picker() {
+      super(ShadowBitmapDrawable.class, ShadowNativeBitmapDrawable.class);
+    }
+  }
+}
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeBitmapFactory.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeBitmapFactory.java
new file mode 100644
index 0000000..54213d8
--- /dev/null
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeBitmapFactory.java
@@ -0,0 +1,153 @@
+package org.robolectric.shadows;
+
+import static android.os.Build.VERSION_CODES.LOLLIPOP;
+import static android.os.Build.VERSION_CODES.O;
+import static android.os.Build.VERSION_CODES.P;
+import static android.os.Build.VERSION_CODES.Q;
+import static org.robolectric.util.reflector.Reflector.reflector;
+
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.BitmapFactory.Options;
+import android.graphics.Rect;
+import java.io.FileDescriptor;
+import java.io.InputStream;
+import org.robolectric.annotation.Implementation;
+import org.robolectric.annotation.Implements;
+import org.robolectric.nativeruntime.BitmapFactoryNatives;
+import org.robolectric.nativeruntime.DefaultNativeRuntimeLoader;
+import org.robolectric.shadow.api.Shadow;
+import org.robolectric.shadows.ShadowNativeBitmapFactory.Picker;
+import org.robolectric.util.reflector.Direct;
+import org.robolectric.util.reflector.ForType;
+
+/** Shadow for {@link BitmapFactory} that is backed by native code */
+@Implements(
+    value = BitmapFactory.class,
+    minSdk = O,
+    shadowPicker = Picker.class,
+    isInAndroidSdk = false)
+public class ShadowNativeBitmapFactory {
+
+  static {
+    DefaultNativeRuntimeLoader.injectAndLoad();
+  }
+
+  @Implementation
+  protected static Bitmap decodeResource(Resources res, int id, BitmapFactory.Options options) {
+    Bitmap bitmap = reflector(BitmapFactoryReflector.class).decodeResource(res, id, options);
+    if (bitmap == null) {
+      return null;
+    }
+
+    ShadowNativeBitmap shadowNativeBitmap = Shadow.extract(bitmap);
+    shadowNativeBitmap.setCreatedFromResId(id);
+    return bitmap;
+  }
+
+  @Implementation
+  protected static Bitmap decodeStream(InputStream is, Rect outPadding, Options opts) {
+    reflector(BitmapFactoryOptionsReflector.class).validate(opts);
+    Bitmap bitmap =
+        reflector(BitmapFactoryReflector.class).decodeStreamInternal(is, outPadding, opts);
+    reflector(BitmapFactoryReflector.class).setDensityFromOptions(bitmap, opts);
+    return bitmap;
+  }
+
+  @Implementation(minSdk = Q)
+  protected static Bitmap nativeDecodeStream(
+      InputStream is,
+      byte[] storage,
+      Rect padding,
+      Options opts,
+      long inBitmapHandle,
+      long colorSpaceHandle) {
+    return BitmapFactoryNatives.nativeDecodeStream(
+        is, storage, padding, opts, inBitmapHandle, colorSpaceHandle);
+  }
+
+  @Implementation(maxSdk = P)
+  protected static Bitmap nativeDecodeStream(
+      InputStream is, byte[] storage, Rect padding, Options opts) {
+    return nativeDecodeStream(is, storage, padding, opts, nativeInBitmap(opts), 0);
+  }
+
+  @Implementation(minSdk = Q)
+  protected static Bitmap nativeDecodeFileDescriptor(
+      FileDescriptor fd, Rect padding, Options opts, long inBitmapHandle, long colorSpaceHandle) {
+    return BitmapFactoryNatives.nativeDecodeFileDescriptor(
+        fd, padding, opts, inBitmapHandle, colorSpaceHandle);
+  }
+
+  @Implementation(maxSdk = P)
+  protected static Bitmap nativeDecodeFileDescriptor(
+      FileDescriptor fd, Rect padding, Options opts) {
+    return nativeDecodeFileDescriptor(fd, padding, opts, nativeInBitmap(opts), 0);
+  }
+
+  @Implementation(minSdk = Q)
+  protected static Bitmap nativeDecodeAsset(
+      long nativeAsset, Rect padding, Options opts, long inBitmapHandle, long colorSpaceHandle) {
+    return BitmapFactoryNatives.nativeDecodeAsset(
+        nativeAsset, padding, opts, inBitmapHandle, colorSpaceHandle);
+  }
+
+  @Implementation(minSdk = LOLLIPOP, maxSdk = P)
+  protected static Bitmap nativeDecodeAsset(long nativeAsset, Rect padding, Options opts) {
+    return nativeDecodeAsset(nativeAsset, padding, opts, nativeInBitmap(opts), 0);
+  }
+
+  @Implementation(minSdk = Q)
+  protected static Bitmap nativeDecodeByteArray(
+      byte[] data,
+      int offset,
+      int length,
+      Options opts,
+      long inBitmapHandle,
+      long colorSpaceHandle) {
+    return BitmapFactoryNatives.nativeDecodeByteArray(
+        data, offset, length, opts, inBitmapHandle, colorSpaceHandle);
+  }
+
+  @Implementation(maxSdk = P)
+  protected static Bitmap nativeDecodeByteArray(byte[] data, int offset, int length, Options opts) {
+    return nativeDecodeByteArray(data, offset, length, opts, nativeInBitmap(opts), 0);
+  }
+
+  @Implementation
+  protected static boolean nativeIsSeekable(FileDescriptor fd) {
+    return BitmapFactoryNatives.nativeIsSeekable(fd);
+  }
+
+  /** Helper for passing inBitmap's native pointer to native. */
+  static long nativeInBitmap(Options opts) {
+    if (opts == null || opts.inBitmap == null) {
+      return 0;
+    }
+
+    return opts.inBitmap.getNativeInstance();
+  }
+
+  @ForType(BitmapFactory.class)
+  interface BitmapFactoryReflector {
+    Bitmap decodeStreamInternal(InputStream is, Rect outPadding, Options opts);
+
+    void setDensityFromOptions(Bitmap outputBitmap, Options opts);
+
+    @Direct
+    Bitmap decodeResource(Resources res, int id, BitmapFactory.Options options);
+  }
+
+  @ForType(BitmapFactory.Options.class)
+  interface BitmapFactoryOptionsReflector {
+    void validate(Options opts);
+  }
+
+  /** Shadow picker for {@link BitmapFactory}. */
+  public static final class Picker extends GraphicsShadowPicker<Object> {
+    public Picker() {
+      super(ShadowBitmapFactory.class, ShadowNativeBitmapFactory.class);
+    }
+  }
+}
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeBitmapShader.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeBitmapShader.java
new file mode 100644
index 0000000..1c4aa80
--- /dev/null
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeBitmapShader.java
@@ -0,0 +1,69 @@
+package org.robolectric.shadows;
+
+import static android.os.Build.VERSION_CODES.O;
+import static android.os.Build.VERSION_CODES.P;
+import static android.os.Build.VERSION_CODES.Q;
+import static android.os.Build.VERSION_CODES.R;
+import static android.os.Build.VERSION_CODES.S;
+import static android.os.Build.VERSION_CODES.S_V2;
+import static android.os.Build.VERSION_CODES.TIRAMISU;
+
+import android.graphics.Bitmap;
+import android.graphics.BitmapShader;
+import org.robolectric.annotation.Implementation;
+import org.robolectric.annotation.Implements;
+import org.robolectric.nativeruntime.BitmapShaderNatives;
+import org.robolectric.nativeruntime.DefaultNativeRuntimeLoader;
+import org.robolectric.shadows.ShadowNativeBitmapShader.Picker;
+
+/** Shadow for {@link BitmapShader} that is backed by native code */
+@Implements(value = BitmapShader.class, minSdk = O, shadowPicker = Picker.class)
+public class ShadowNativeBitmapShader {
+
+  @Implementation(minSdk = O, maxSdk = P)
+  protected static long nativeCreate(
+      long nativeMatrix, Bitmap bitmap, int shaderTileModeX, int shaderTileModeY) {
+    return nativeCreate(
+        nativeMatrix,
+        bitmap != null ? bitmap.getNativeInstance() : 0,
+        shaderTileModeX,
+        shaderTileModeY,
+        false);
+  }
+
+  @Implementation(minSdk = Q, maxSdk = R)
+  protected static long nativeCreate(
+      long nativeMatrix, long bitmapHandle, int shaderTileModeX, int shaderTileModeY) {
+    return nativeCreate(nativeMatrix, bitmapHandle, shaderTileModeX, shaderTileModeY, false);
+  }
+
+  @Implementation(minSdk = S, maxSdk = S_V2)
+  protected static long nativeCreate(
+      long nativeMatrix,
+      long bitmapHandle,
+      int shaderTileModeX,
+      int shaderTileModeY,
+      boolean filter) {
+    DefaultNativeRuntimeLoader.injectAndLoad();
+    return BitmapShaderNatives.nativeCreate(
+        nativeMatrix, bitmapHandle, shaderTileModeX, shaderTileModeY, filter);
+  }
+
+  @Implementation(minSdk = TIRAMISU)
+  protected static long nativeCreate(
+      long nativeMatrix,
+      long bitmapHandle,
+      int shaderTileModeX,
+      int shaderTileModeY,
+      boolean filter,
+      boolean isDirectSampled) {
+    return nativeCreate(nativeMatrix, bitmapHandle, shaderTileModeX, shaderTileModeY, filter);
+  }
+
+  /** Shadow picker for {@link BitmapShader}. */
+  public static final class Picker extends GraphicsShadowPicker<Object> {
+    public Picker() {
+      super(null, ShadowNativeBitmapShader.class);
+    }
+  }
+}
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeBlendModeColorFilter.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeBlendModeColorFilter.java
new file mode 100644
index 0000000..f4cbf99
--- /dev/null
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeBlendModeColorFilter.java
@@ -0,0 +1,29 @@
+package org.robolectric.shadows;
+
+import static android.os.Build.VERSION_CODES.O;
+import static android.os.Build.VERSION_CODES.Q;
+
+import android.graphics.BlendModeColorFilter;
+import org.robolectric.annotation.Implementation;
+import org.robolectric.annotation.Implements;
+import org.robolectric.nativeruntime.BlendModeColorFilterNatives;
+import org.robolectric.nativeruntime.DefaultNativeRuntimeLoader;
+import org.robolectric.shadows.ShadowNativeBlendModeColorFilter.Picker;
+
+/** Shadow for {@link BlendModeColorFilter} that is backed by native code */
+@Implements(value = BlendModeColorFilter.class, minSdk = O, shadowPicker = Picker.class)
+public class ShadowNativeBlendModeColorFilter {
+
+  @Implementation(minSdk = Q)
+  protected static long native_CreateBlendModeFilter(int srcColor, int blendmode) {
+    DefaultNativeRuntimeLoader.injectAndLoad();
+    return BlendModeColorFilterNatives.native_CreateBlendModeFilter(srcColor, blendmode);
+  }
+
+  /** Shadow picker for {@link BlendModeColorFilter}. */
+  public static final class Picker extends GraphicsShadowPicker<Object> {
+    public Picker() {
+      super(null, ShadowNativeBlendModeColorFilter.class);
+    }
+  }
+}
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeBlurMaskFilter.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeBlurMaskFilter.java
new file mode 100644
index 0000000..77d4a9d
--- /dev/null
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeBlurMaskFilter.java
@@ -0,0 +1,28 @@
+package org.robolectric.shadows;
+
+import static android.os.Build.VERSION_CODES.O;
+
+import android.graphics.BlurMaskFilter;
+import org.robolectric.annotation.Implementation;
+import org.robolectric.annotation.Implements;
+import org.robolectric.nativeruntime.BlurMaskFilterNatives;
+import org.robolectric.nativeruntime.DefaultNativeRuntimeLoader;
+import org.robolectric.shadows.ShadowNativeBlurMaskFilter.Picker;
+
+/** Shadow for {@link BlurMaskFilter} that is backed by native code */
+@Implements(value = BlurMaskFilter.class, minSdk = O, shadowPicker = Picker.class)
+public class ShadowNativeBlurMaskFilter {
+
+  @Implementation(minSdk = O)
+  protected static long nativeConstructor(float radius, int style) {
+    DefaultNativeRuntimeLoader.injectAndLoad();
+    return BlurMaskFilterNatives.nativeConstructor(radius, style);
+  }
+
+  /** Shadow picker for {@link BlurMaskFilter}. */
+  public static final class Picker extends GraphicsShadowPicker<Object> {
+    public Picker() {
+      super(null, ShadowNativeBlurMaskFilter.class);
+    }
+  }
+}
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeCanvas.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeCanvas.java
new file mode 100644
index 0000000..c2dffb8
--- /dev/null
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeCanvas.java
@@ -0,0 +1,209 @@
+package org.robolectric.shadows;
+
+import static android.os.Build.VERSION_CODES.O;
+import static android.os.Build.VERSION_CODES.P;
+import static android.os.Build.VERSION_CODES.Q;
+import static android.os.Build.VERSION_CODES.R;
+import static android.os.Build.VERSION_CODES.S;
+
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Rect;
+import org.robolectric.annotation.Implementation;
+import org.robolectric.annotation.Implements;
+import org.robolectric.nativeruntime.CanvasNatives;
+import org.robolectric.nativeruntime.DefaultNativeRuntimeLoader;
+
+/** Shadow for {@link Canvas} that is backed by native code */
+@Implements(value = Canvas.class, minSdk = O, isInAndroidSdk = false)
+public class ShadowNativeCanvas extends ShadowNativeBaseCanvas {
+
+  @Implementation(minSdk = O)
+  protected static void nFreeCaches() {
+    CanvasNatives.nFreeCaches();
+  }
+
+  @Implementation(minSdk = O)
+  protected static void nFreeTextLayoutCaches() {
+    CanvasNatives.nFreeTextLayoutCaches();
+  }
+
+  @Implementation(minSdk = O)
+  protected static long nGetNativeFinalizer() {
+    return CanvasNatives.nGetNativeFinalizer();
+  }
+
+  @Implementation(minSdk = P)
+  protected static void nSetCompatibilityVersion(int apiLevel) {
+    CanvasNatives.nSetCompatibilityVersion(apiLevel);
+  }
+
+  @Implementation(minSdk = O, maxSdk = P)
+  protected static long nInitRaster(Bitmap bitmap) {
+    return nInitRaster(bitmap != null ? bitmap.getNativeInstance() : 0);
+  }
+
+  @Implementation(minSdk = Q)
+  protected static long nInitRaster(long bitmapHandle) {
+    DefaultNativeRuntimeLoader.injectAndLoad();
+    return CanvasNatives.nInitRaster(bitmapHandle);
+  }
+
+  @Implementation(minSdk = O, maxSdk = P)
+  protected static void nSetBitmap(long canvasHandle, Bitmap bitmap) {
+    CanvasNatives.nSetBitmap(canvasHandle, bitmap != null ? bitmap.getNativeInstance() : 0);
+  }
+
+  @Implementation(minSdk = Q)
+  protected static void nSetBitmap(long canvasHandle, long bitmapHandle) {
+    CanvasNatives.nSetBitmap(canvasHandle, bitmapHandle);
+  }
+
+  @Implementation(minSdk = O)
+  protected static boolean nGetClipBounds(long nativeCanvas, Rect bounds) {
+    return CanvasNatives.nGetClipBounds(nativeCanvas, bounds);
+  }
+
+  @Implementation(minSdk = O)
+  protected static boolean nIsOpaque(long canvasHandle) {
+    return CanvasNatives.nIsOpaque(canvasHandle);
+  }
+
+  @Implementation(minSdk = O)
+  protected static int nGetWidth(long canvasHandle) {
+    return CanvasNatives.nGetWidth(canvasHandle);
+  }
+
+  @Implementation(minSdk = O)
+  protected static int nGetHeight(long canvasHandle) {
+    return CanvasNatives.nGetHeight(canvasHandle);
+  }
+
+  @Implementation(minSdk = O)
+  protected static int nSave(long canvasHandle, int saveFlags) {
+    return CanvasNatives.nSave(canvasHandle, saveFlags);
+  }
+
+  @Implementation(minSdk = S)
+  protected static int nSaveLayer(
+      long nativeCanvas, float l, float t, float r, float b, long nativePaint) {
+    return CanvasNatives.nSaveLayer(nativeCanvas, l, t, r, b, nativePaint);
+  }
+
+  @Implementation(minSdk = O, maxSdk = R)
+  protected static int nSaveLayer(
+      long nativeCanvas, float l, float t, float r, float b, long nativePaint, int layerFlags) {
+    return nSaveLayer(nativeCanvas, l, t, r, b, nativePaint);
+  }
+
+  @Implementation(minSdk = S)
+  protected static int nSaveLayerAlpha(
+      long nativeCanvas, float l, float t, float r, float b, int alpha) {
+    return CanvasNatives.nSaveLayerAlpha(nativeCanvas, l, t, r, b, alpha);
+  }
+
+  @Implementation(minSdk = O, maxSdk = R)
+  protected static int nSaveLayerAlpha(
+      long nativeCanvas, float l, float t, float r, float b, int alpha, int layerFlags) {
+    return nSaveLayerAlpha(nativeCanvas, l, t, r, b, alpha);
+  }
+
+  @Implementation(minSdk = Q)
+  protected static int nSaveUnclippedLayer(long nativeCanvas, int l, int t, int r, int b) {
+    return CanvasNatives.nSaveUnclippedLayer(nativeCanvas, l, t, r, b);
+  }
+
+  @Implementation(minSdk = Q)
+  protected static void nRestoreUnclippedLayer(long nativeCanvas, int saveCount, long nativePaint) {
+    CanvasNatives.nRestoreUnclippedLayer(nativeCanvas, saveCount, nativePaint);
+  }
+
+  @Implementation(minSdk = O)
+  protected static boolean nRestore(long canvasHandle) {
+    return CanvasNatives.nRestore(canvasHandle);
+  }
+
+  @Implementation(minSdk = O)
+  protected static void nRestoreToCount(long canvasHandle, int saveCount) {
+    CanvasNatives.nRestoreToCount(canvasHandle, saveCount);
+  }
+
+  @Implementation(minSdk = O)
+  protected static int nGetSaveCount(long canvasHandle) {
+    return CanvasNatives.nGetSaveCount(canvasHandle);
+  }
+
+  @Implementation(minSdk = O)
+  protected static void nTranslate(long canvasHandle, float dx, float dy) {
+    CanvasNatives.nTranslate(canvasHandle, dx, dy);
+  }
+
+  @Implementation(minSdk = O)
+  protected static void nScale(long canvasHandle, float sx, float sy) {
+    CanvasNatives.nScale(canvasHandle, sx, sy);
+  }
+
+  @Implementation(minSdk = O)
+  protected static void nRotate(long canvasHandle, float degrees) {
+    CanvasNatives.nRotate(canvasHandle, degrees);
+  }
+
+  @Implementation(minSdk = O)
+  protected static void nSkew(long canvasHandle, float sx, float sy) {
+    CanvasNatives.nSkew(canvasHandle, sx, sy);
+  }
+
+  @Implementation(minSdk = O)
+  protected static void nConcat(long nativeCanvas, long nativeMatrix) {
+    CanvasNatives.nConcat(nativeCanvas, nativeMatrix);
+  }
+
+  @Implementation(minSdk = O)
+  protected static void nSetMatrix(long nativeCanvas, long nativeMatrix) {
+    CanvasNatives.nSetMatrix(nativeCanvas, nativeMatrix);
+  }
+
+  @Implementation(minSdk = O)
+  protected static boolean nClipRect(
+      long nativeCanvas, float left, float top, float right, float bottom, int regionOp) {
+    return CanvasNatives.nClipRect(nativeCanvas, left, top, right, bottom, regionOp);
+  }
+
+  @Implementation(minSdk = O)
+  protected static boolean nClipPath(long nativeCanvas, long nativePath, int regionOp) {
+    return CanvasNatives.nClipPath(nativeCanvas, nativePath, regionOp);
+  }
+
+  @Implementation(minSdk = O)
+  protected static void nSetDrawFilter(long nativeCanvas, long nativeFilter) {
+    CanvasNatives.nSetDrawFilter(nativeCanvas, nativeFilter);
+  }
+
+  @Implementation(minSdk = O)
+  protected static void nGetMatrix(long nativeCanvas, long nativeMatrix) {
+    CanvasNatives.nGetMatrix(nativeCanvas, nativeMatrix);
+  }
+
+  @Implementation(minSdk = O)
+  protected static boolean nQuickReject(long nativeCanvas, long nativePath) {
+    return CanvasNatives.nQuickReject(nativeCanvas, nativePath);
+  }
+
+  @Implementation(minSdk = O)
+  protected static boolean nQuickReject(
+      long nativeCanvas, float left, float top, float right, float bottom) {
+    return CanvasNatives.nQuickReject(nativeCanvas, left, top, right, bottom);
+  }
+
+  /**
+   * In Android P and below, Canvas.saveUnclippedLayer called {@link
+   * ShadowNativeCanvas#nSaveLayer(long, float, float, float, float, long)}.
+   *
+   * <p>However, in Android Q, a new native method was added specifically to save unclipped layers.
+   * Use this new method to fix things like ScrollView fade effects in P and below.
+   */
+  @Implementation(minSdk = P, maxSdk = P)
+  protected int saveUnclippedLayer(int left, int top, int right, int bottom) {
+    return nSaveUnclippedLayer(getNativeCanvas(), left, top, right, bottom);
+  }
+}
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeCanvasProperty.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeCanvasProperty.java
new file mode 100644
index 0000000..8bb4f16
--- /dev/null
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeCanvasProperty.java
@@ -0,0 +1,38 @@
+package org.robolectric.shadows;
+
+import static android.os.Build.VERSION_CODES.O;
+
+import android.graphics.CanvasProperty;
+import org.robolectric.annotation.Implementation;
+import org.robolectric.annotation.Implements;
+import org.robolectric.nativeruntime.CanvasPropertyNatives;
+import org.robolectric.nativeruntime.DefaultNativeRuntimeLoader;
+import org.robolectric.shadows.ShadowNativeCanvasProperty.Picker;
+
+/** Shadow for {@link CanvasProperty} that is backed by native code */
+@Implements(
+    value = CanvasProperty.class,
+    minSdk = O,
+    shadowPicker = Picker.class,
+    isInAndroidSdk = false)
+public class ShadowNativeCanvasProperty<T> {
+
+  @Implementation
+  protected static long nCreateFloat(float initialValue) {
+    DefaultNativeRuntimeLoader.injectAndLoad();
+    return CanvasPropertyNatives.nCreateFloat(initialValue);
+  }
+
+  @Implementation
+  protected static long nCreatePaint(long initialValuePaintPtr) {
+    DefaultNativeRuntimeLoader.injectAndLoad();
+    return CanvasPropertyNatives.nCreatePaint(initialValuePaintPtr);
+  }
+
+  /** Shadow picker for {@link CanvasProperty}. */
+  public static final class Picker extends GraphicsShadowPicker<Object> {
+    public Picker() {
+      super(null, ShadowNativeCanvasProperty.class);
+    }
+  }
+}
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeColor.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeColor.java
new file mode 100644
index 0000000..05e3aa3
--- /dev/null
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeColor.java
@@ -0,0 +1,34 @@
+package org.robolectric.shadows;
+
+import static android.os.Build.VERSION_CODES.O;
+
+import android.graphics.Color;
+import org.robolectric.annotation.Implementation;
+import org.robolectric.annotation.Implements;
+import org.robolectric.nativeruntime.ColorNatives;
+import org.robolectric.nativeruntime.DefaultNativeRuntimeLoader;
+import org.robolectric.shadows.ShadowNativeColor.Picker;
+
+/** Shadow for {@link Color} that is backed by native code */
+@Implements(value = Color.class, minSdk = O, shadowPicker = Picker.class, isInAndroidSdk = false)
+public class ShadowNativeColor {
+
+  @Implementation(minSdk = O)
+  protected static void nativeRGBToHSV(int red, int greed, int blue, float[] hsv) {
+    DefaultNativeRuntimeLoader.injectAndLoad();
+    ColorNatives.nativeRGBToHSV(red, greed, blue, hsv);
+  }
+
+  @Implementation(minSdk = O)
+  protected static int nativeHSVToColor(int alpha, float[] hsv) {
+    DefaultNativeRuntimeLoader.injectAndLoad();
+    return ColorNatives.nativeHSVToColor(alpha, hsv);
+  }
+
+  /** Shadow picker for {@link Color}. */
+  public static final class Picker extends GraphicsShadowPicker<Object> {
+    public Picker() {
+      super(ShadowColor.class, ShadowNativeColor.class);
+    }
+  }
+}
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeColorFilter.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeColorFilter.java
new file mode 100644
index 0000000..ed641a2
--- /dev/null
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeColorFilter.java
@@ -0,0 +1,32 @@
+package org.robolectric.shadows;
+
+import static android.os.Build.VERSION_CODES.O;
+import static android.os.Build.VERSION_CODES.O_MR1;
+
+import android.graphics.ColorFilter;
+import org.robolectric.annotation.Implementation;
+import org.robolectric.annotation.Implements;
+import org.robolectric.nativeruntime.ColorFilterNatives;
+import org.robolectric.shadows.ShadowNativeColorFilter.Picker;
+
+/** Shadow for {@link ColorFilter} that is backed by native code */
+@Implements(value = ColorFilter.class, minSdk = O, shadowPicker = Picker.class)
+public class ShadowNativeColorFilter {
+
+  @Implementation(minSdk = O_MR1)
+  protected static long nativeGetFinalizer() {
+    return ColorFilterNatives.nativeGetFinalizer();
+  }
+
+  @Implementation(minSdk = O, maxSdk = O)
+  protected static void nSafeUnref(long nativeInstance) {
+    ColorFilterNatives.nSafeUnref(nativeInstance);
+  }
+
+  /** Shadow picker for {@link ColorFilter}. */
+  public static final class Picker extends GraphicsShadowPicker<Object> {
+    public Picker() {
+      super(null, ShadowNativeColorFilter.class);
+    }
+  }
+}
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeColorMatrixColorFilter.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeColorMatrixColorFilter.java
new file mode 100644
index 0000000..307303b
--- /dev/null
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeColorMatrixColorFilter.java
@@ -0,0 +1,28 @@
+package org.robolectric.shadows;
+
+import static android.os.Build.VERSION_CODES.O;
+
+import android.graphics.ColorMatrixColorFilter;
+import org.robolectric.annotation.Implementation;
+import org.robolectric.annotation.Implements;
+import org.robolectric.nativeruntime.ColorMatrixColorFilterNatives;
+import org.robolectric.nativeruntime.DefaultNativeRuntimeLoader;
+import org.robolectric.shadows.ShadowNativeColorMatrixColorFilter.Picker;
+
+/** Shadow for {@link ColorMatrixColorFilter} that is backed by native code */
+@Implements(value = ColorMatrixColorFilter.class, minSdk = O, shadowPicker = Picker.class)
+public class ShadowNativeColorMatrixColorFilter {
+
+  @Implementation(minSdk = O)
+  protected static long nativeColorMatrixFilter(float[] array) {
+    DefaultNativeRuntimeLoader.injectAndLoad();
+    return ColorMatrixColorFilterNatives.nativeColorMatrixFilter(array);
+  }
+
+  /** Shadow picker for {@link ColorMatrixColorFilter}. */
+  public static final class Picker extends GraphicsShadowPicker<Object> {
+    public Picker() {
+      super(null, ShadowNativeColorMatrixColorFilter.class);
+    }
+  }
+}
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeColorSpaceRgb.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeColorSpaceRgb.java
new file mode 100644
index 0000000..6bb12ee
--- /dev/null
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeColorSpaceRgb.java
@@ -0,0 +1,39 @@
+package org.robolectric.shadows;
+
+import static android.os.Build.VERSION_CODES.O;
+import static android.os.Build.VERSION_CODES.Q;
+
+import android.graphics.ColorSpace;
+import org.robolectric.annotation.Implementation;
+import org.robolectric.annotation.Implements;
+import org.robolectric.nativeruntime.ColorSpaceRgbNatives;
+import org.robolectric.nativeruntime.DefaultNativeRuntimeLoader;
+import org.robolectric.shadows.ShadowNativeColorSpaceRgb.Picker;
+
+/** Shadow for {@link ColorSpace.Rgb} that is backed by native code */
+@Implements(
+    value = ColorSpace.Rgb.class,
+    minSdk = O,
+    shadowPicker = Picker.class,
+    isInAndroidSdk = false)
+public class ShadowNativeColorSpaceRgb {
+
+  @Implementation(minSdk = Q)
+  protected static long nativeGetNativeFinalizer() {
+    return ColorSpaceRgbNatives.nativeGetNativeFinalizer();
+  }
+
+  @Implementation(minSdk = Q)
+  protected static long nativeCreate(
+      float a, float b, float c, float d, float e, float f, float g, float[] xyz) {
+    DefaultNativeRuntimeLoader.injectAndLoad();
+    return ColorSpaceRgbNatives.nativeCreate(a, b, c, d, e, f, g, xyz);
+  }
+
+  /** Shadow picker for {@link ColorSpace.Rgb}. */
+  public static final class Picker extends GraphicsShadowPicker<Object> {
+    public Picker() {
+      super(ShadowColorSpaceRgb.class, ShadowNativeColorSpaceRgb.class);
+    }
+  }
+}
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeComposePathEffect.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeComposePathEffect.java
new file mode 100644
index 0000000..6bf1e3f
--- /dev/null
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeComposePathEffect.java
@@ -0,0 +1,28 @@
+package org.robolectric.shadows;
+
+import static android.os.Build.VERSION_CODES.O;
+
+import android.graphics.ComposePathEffect;
+import org.robolectric.annotation.Implementation;
+import org.robolectric.annotation.Implements;
+import org.robolectric.nativeruntime.ComposePathEffectNatives;
+import org.robolectric.nativeruntime.DefaultNativeRuntimeLoader;
+import org.robolectric.shadows.ShadowNativeComposePathEffect.Picker;
+
+/** Shadow for {@link ComposePathEffect} that is backed by native code */
+@Implements(value = ComposePathEffect.class, minSdk = O, shadowPicker = Picker.class)
+public class ShadowNativeComposePathEffect {
+
+  @Implementation(minSdk = O)
+  protected static long nativeCreate(long nativeOuterpe, long nativeInnerpe) {
+    DefaultNativeRuntimeLoader.injectAndLoad();
+    return ComposePathEffectNatives.nativeCreate(nativeOuterpe, nativeInnerpe);
+  }
+
+  /** Shadow picker for {@link ComposePathEffect}. */
+  public static final class Picker extends GraphicsShadowPicker<Object> {
+    public Picker() {
+      super(null, ShadowNativeComposePathEffect.class);
+    }
+  }
+}
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeComposeShader.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeComposeShader.java
new file mode 100644
index 0000000..b5e5b4a
--- /dev/null
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeComposeShader.java
@@ -0,0 +1,30 @@
+package org.robolectric.shadows;
+
+import static android.os.Build.VERSION_CODES.O;
+
+import android.graphics.ComposeShader;
+import org.robolectric.annotation.Implementation;
+import org.robolectric.annotation.Implements;
+import org.robolectric.nativeruntime.ComposeShaderNatives;
+import org.robolectric.nativeruntime.DefaultNativeRuntimeLoader;
+import org.robolectric.shadows.ShadowNativeComposeShader.Picker;
+
+/** Shadow for {@link ComposeShader} that is backed by native code */
+@Implements(value = ComposeShader.class, minSdk = O, shadowPicker = Picker.class)
+public class ShadowNativeComposeShader {
+
+  @Implementation(minSdk = O)
+  protected static long nativeCreate(
+      long nativeMatrix, long nativeShaderA, long nativeShaderB, int porterDuffMode) {
+    DefaultNativeRuntimeLoader.injectAndLoad();
+    return ComposeShaderNatives.nativeCreate(
+        nativeMatrix, nativeShaderA, nativeShaderB, porterDuffMode);
+  }
+
+  /** Shadow picker for {@link ComposeShader}. */
+  public static final class Picker extends GraphicsShadowPicker<Object> {
+    public Picker() {
+      super(null, ShadowNativeComposeShader.class);
+    }
+  }
+}
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeCornerPathEffect.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeCornerPathEffect.java
new file mode 100644
index 0000000..7c30629
--- /dev/null
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeCornerPathEffect.java
@@ -0,0 +1,28 @@
+package org.robolectric.shadows;
+
+import static android.os.Build.VERSION_CODES.O;
+
+import android.graphics.CornerPathEffect;
+import org.robolectric.annotation.Implementation;
+import org.robolectric.annotation.Implements;
+import org.robolectric.nativeruntime.CornerPathEffectNatives;
+import org.robolectric.nativeruntime.DefaultNativeRuntimeLoader;
+import org.robolectric.shadows.ShadowNativeCornerPathEffect.Picker;
+
+/** Shadow for {@link CornerPathEffect} that is backed by native code */
+@Implements(value = CornerPathEffect.class, minSdk = O, shadowPicker = Picker.class)
+public class ShadowNativeCornerPathEffect {
+
+  @Implementation(minSdk = O)
+  protected static long nativeCreate(float radius) {
+    DefaultNativeRuntimeLoader.injectAndLoad();
+    return CornerPathEffectNatives.nativeCreate(radius);
+  }
+
+  /** Shadow picker for {@link CornerPathEffect}. */
+  public static final class Picker extends GraphicsShadowPicker<Object> {
+    public Picker() {
+      super(null, ShadowNativeCornerPathEffect.class);
+    }
+  }
+}
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeDashPathEffect.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeDashPathEffect.java
new file mode 100644
index 0000000..d8ad1eb
--- /dev/null
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeDashPathEffect.java
@@ -0,0 +1,28 @@
+package org.robolectric.shadows;
+
+import static android.os.Build.VERSION_CODES.O;
+
+import android.graphics.DashPathEffect;
+import org.robolectric.annotation.Implementation;
+import org.robolectric.annotation.Implements;
+import org.robolectric.nativeruntime.DashPathEffectNatives;
+import org.robolectric.nativeruntime.DefaultNativeRuntimeLoader;
+import org.robolectric.shadows.ShadowNativeDashPathEffect.Picker;
+
+/** Shadow for {@link DashPathEffect} that is backed by native code */
+@Implements(value = DashPathEffect.class, minSdk = O, shadowPicker = Picker.class)
+public class ShadowNativeDashPathEffect {
+
+  @Implementation(minSdk = O)
+  protected static long nativeCreate(float[] intervals, float phase) {
+    DefaultNativeRuntimeLoader.injectAndLoad();
+    return DashPathEffectNatives.nativeCreate(intervals, phase);
+  }
+
+  /** Shadow picker for {@link DashPathEffect}. */
+  public static final class Picker extends GraphicsShadowPicker<Object> {
+    public Picker() {
+      super(null, ShadowNativeDashPathEffect.class);
+    }
+  }
+}
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeDiscretePathEffect.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeDiscretePathEffect.java
new file mode 100644
index 0000000..b7f5e3e
--- /dev/null
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeDiscretePathEffect.java
@@ -0,0 +1,28 @@
+package org.robolectric.shadows;
+
+import static android.os.Build.VERSION_CODES.O;
+
+import android.graphics.DiscretePathEffect;
+import org.robolectric.annotation.Implementation;
+import org.robolectric.annotation.Implements;
+import org.robolectric.nativeruntime.DefaultNativeRuntimeLoader;
+import org.robolectric.nativeruntime.DiscretePathEffectNatives;
+import org.robolectric.shadows.ShadowNativeDiscretePathEffect.Picker;
+
+/** Shadow for {@link DiscretePathEffect} that is backed by native code */
+@Implements(value = DiscretePathEffect.class, minSdk = O, shadowPicker = Picker.class)
+public class ShadowNativeDiscretePathEffect {
+
+  @Implementation(minSdk = O)
+  protected static long nativeCreate(float length, float deviation) {
+    DefaultNativeRuntimeLoader.injectAndLoad();
+    return DiscretePathEffectNatives.nativeCreate(length, deviation);
+  }
+
+  /** Shadow picker for {@link DiscretePathEffect}. */
+  public static final class Picker extends GraphicsShadowPicker<Object> {
+    public Picker() {
+      super(null, ShadowNativeDiscretePathEffect.class);
+    }
+  }
+}
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeDisplayListCanvas.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeDisplayListCanvas.java
new file mode 100644
index 0000000..f369ca5
--- /dev/null
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeDisplayListCanvas.java
@@ -0,0 +1,72 @@
+package org.robolectric.shadows;
+
+import static android.os.Build.VERSION_CODES.O;
+import static android.os.Build.VERSION_CODES.P;
+
+import org.robolectric.annotation.Implementation;
+import org.robolectric.annotation.Implements;
+import org.robolectric.nativeruntime.DefaultNativeRuntimeLoader;
+import org.robolectric.nativeruntime.RecordingCanvasNatives;
+import org.robolectric.shadows.ShadowNativeDisplayListCanvas.Picker;
+
+/** Shadow for {@link android.view.DisplayListCanvas} that is backed by native code */
+@Implements(
+    className = "android.view.DisplayListCanvas",
+    minSdk = O,
+    maxSdk = P,
+    shadowPicker = Picker.class)
+public class ShadowNativeDisplayListCanvas extends ShadowNativeRecordingCanvas {
+
+  @Implementation
+  protected static long nCreateDisplayListCanvas(long node, int width, int height) {
+    DefaultNativeRuntimeLoader.injectAndLoad();
+    return RecordingCanvasNatives.nCreateDisplayListCanvas(node, width, height);
+  }
+
+  @Implementation
+  protected static void nResetDisplayListCanvas(long canvas, long node, int width, int height) {
+    RecordingCanvasNatives.nResetDisplayListCanvas(canvas, node, width, height);
+  }
+
+  @Implementation
+  protected static int nGetMaximumTextureWidth() {
+    return RecordingCanvasNatives.nGetMaximumTextureWidth();
+  }
+
+  @Implementation
+  protected static int nGetMaximumTextureHeight() {
+    return RecordingCanvasNatives.nGetMaximumTextureHeight();
+  }
+
+  @Implementation
+  protected static void nDrawRenderNode(long renderer, long renderNode) {
+    RecordingCanvasNatives.nDrawRenderNode(renderer, renderNode);
+  }
+
+  @Implementation
+  protected static void nDrawCircle(
+      long renderer, long propCx, long propCy, long propRadius, long propPaint) {
+    RecordingCanvasNatives.nDrawCircle(renderer, propCx, propCy, propRadius, propPaint);
+  }
+
+  @Implementation
+  protected static void nDrawRoundRect(
+      long renderer,
+      long propLeft,
+      long propTop,
+      long propRight,
+      long propBottom,
+      long propRx,
+      long propRy,
+      long propPaint) {
+    RecordingCanvasNatives.nDrawRoundRect(
+        renderer, propLeft, propTop, propRight, propBottom, propRx, propRy, propPaint);
+  }
+
+  /** Shadow picker for {@link android.view.DisplayListCanvas}. */
+  public static final class Picker extends GraphicsShadowPicker<Object> {
+    public Picker() {
+      super(ShadowDisplayListCanvas.class, ShadowNativeDisplayListCanvas.class);
+    }
+  }
+}
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeEmbossMaskFilter.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeEmbossMaskFilter.java
new file mode 100644
index 0000000..52ed28e
--- /dev/null
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeEmbossMaskFilter.java
@@ -0,0 +1,29 @@
+package org.robolectric.shadows;
+
+import static android.os.Build.VERSION_CODES.O;
+
+import android.graphics.EmbossMaskFilter;
+import org.robolectric.annotation.Implementation;
+import org.robolectric.annotation.Implements;
+import org.robolectric.nativeruntime.DefaultNativeRuntimeLoader;
+import org.robolectric.nativeruntime.EmbossMaskFilterNatives;
+import org.robolectric.shadows.ShadowNativeEmbossMaskFilter.Picker;
+
+/** Shadow for {@link EmbossMaskFilter} that is backed by native code */
+@Implements(value = EmbossMaskFilter.class, minSdk = O, shadowPicker = Picker.class)
+public class ShadowNativeEmbossMaskFilter {
+
+  @Implementation(minSdk = O)
+  protected static long nativeConstructor(
+      float[] direction, float ambient, float specular, float blurRadius) {
+    DefaultNativeRuntimeLoader.injectAndLoad();
+    return EmbossMaskFilterNatives.nativeConstructor(direction, ambient, specular, blurRadius);
+  }
+
+  /** Shadow picker for {@link EmbossMaskFilter}. */
+  public static final class Picker extends GraphicsShadowPicker<Object> {
+    public Picker() {
+      super(null, ShadowNativeEmbossMaskFilter.class);
+    }
+  }
+}
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeFont.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeFont.java
new file mode 100644
index 0000000..2c64de3
--- /dev/null
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeFont.java
@@ -0,0 +1,281 @@
+package org.robolectric.shadows;
+
+import static android.os.Build.VERSION_CODES.P;
+import static android.os.Build.VERSION_CODES.Q;
+import static android.os.Build.VERSION_CODES.R;
+import static android.os.Build.VERSION_CODES.S;
+import static android.os.Build.VERSION_CODES.TIRAMISU;
+import static org.robolectric.util.reflector.Reflector.reflector;
+
+import android.content.res.AssetManager;
+import android.content.res.Resources;
+import android.graphics.Paint;
+import android.graphics.RectF;
+import android.graphics.fonts.Font;
+import android.util.TypedValue;
+import com.google.common.base.Ascii;
+import com.google.common.base.Preconditions;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import org.robolectric.annotation.Implementation;
+import org.robolectric.annotation.Implements;
+import org.robolectric.annotation.RealObject;
+import org.robolectric.nativeruntime.DefaultNativeRuntimeLoader;
+import org.robolectric.nativeruntime.FontBuilderNatives;
+import org.robolectric.nativeruntime.FontNatives;
+import org.robolectric.shadows.ShadowNativeFont.Picker;
+import org.robolectric.util.reflector.Accessor;
+import org.robolectric.util.reflector.ForType;
+
+/** Shadow for {@link Font} that is backed by native code */
+@Implements(value = Font.class, minSdk = P, shadowPicker = Picker.class, isInAndroidSdk = false)
+public class ShadowNativeFont {
+  @Implementation(minSdk = S)
+  protected static long nGetMinikinFontPtr(long font) {
+    return FontNatives.nGetMinikinFontPtr(font);
+  }
+
+  @Implementation(minSdk = S)
+  protected static long nCloneFont(long font) {
+    return FontNatives.nCloneFont(font);
+  }
+
+  @Implementation(minSdk = S)
+  protected static ByteBuffer nNewByteBuffer(long font) {
+    return FontNatives.nNewByteBuffer(font);
+  }
+
+  @Implementation(minSdk = S)
+  protected static long nGetBufferAddress(long font) {
+    return FontNatives.nGetBufferAddress(font);
+  }
+
+  @Implementation(minSdk = S)
+  protected static int nGetSourceId(long font) {
+    return FontNatives.nGetSourceId(font);
+  }
+
+  @Implementation(minSdk = S)
+  protected static long nGetReleaseNativeFont() {
+    DefaultNativeRuntimeLoader.injectAndLoad();
+    return FontNatives.nGetReleaseNativeFont();
+  }
+
+  @Implementation(minSdk = S)
+  protected static float nGetGlyphBounds(long font, int glyphId, long paint, RectF rect) {
+    return FontNatives.nGetGlyphBounds(font, glyphId, paint, rect);
+  }
+
+  @Implementation(minSdk = S)
+  protected static float nGetFontMetrics(long font, long paint, Paint.FontMetrics metrics) {
+    return FontNatives.nGetFontMetrics(font, paint, metrics);
+  }
+
+  @Implementation(minSdk = S)
+  protected static String nGetFontPath(long fontPtr) {
+    return FontNatives.nGetFontPath(fontPtr);
+  }
+
+  @Implementation(minSdk = S)
+  protected static String nGetLocaleList(long familyPtr) {
+    return FontNatives.nGetLocaleList(familyPtr);
+  }
+
+  @Implementation(minSdk = S)
+  protected static int nGetPackedStyle(long fontPtr) {
+    return FontNatives.nGetPackedStyle(fontPtr);
+  }
+
+  @Implementation(minSdk = S)
+  protected static int nGetIndex(long fontPtr) {
+    return FontNatives.nGetIndex(fontPtr);
+  }
+
+  @Implementation(minSdk = S)
+  protected static int nGetAxisCount(long fontPtr) {
+    return FontNatives.nGetAxisCount(fontPtr);
+  }
+
+  @Implementation(minSdk = S)
+  protected static long nGetAxisInfo(long fontPtr, int i) {
+    return FontNatives.nGetAxisInfo(fontPtr, i);
+  }
+
+  @Implementation(minSdk = S)
+  protected static long[] nGetAvailableFontSet() {
+    return FontNatives.nGetAvailableFontSet();
+  }
+
+  /** Shadow for {@link Font.Builder} that is backed by native code */
+  @Implements(
+      value = Font.Builder.class,
+      minSdk = P,
+      shadowPicker = ShadowNativeFontBuilder.Picker.class,
+      isInAndroidSdk = false)
+  public static class ShadowNativeFontBuilder {
+
+    @RealObject Font.Builder realFontBuilder;
+
+    @Implementation(minSdk = Q, maxSdk = Q)
+    protected void __constructor__(AssetManager am, String path, boolean isAsset, int cookie) {
+      // In Android Q, this method uses native methods that do not exist in later versions, so
+      // they need to be re-implemented using logic from S.
+      reflector(FontBuilderReflector.class, realFontBuilder).setWeight(-1);
+      reflector(FontBuilderReflector.class, realFontBuilder).setItalic(-1);
+      reflector(FontBuilderReflector.class, realFontBuilder).setLocaleList("");
+      try {
+        ByteBuffer buf = createBuffer(am, path, isAsset, cookie);
+        reflector(FontBuilderReflector.class, realFontBuilder).setBuffer(buf);
+      } catch (IOException e) {
+        reflector(FontBuilderReflector.class, realFontBuilder).setException(e);
+      }
+    }
+
+    @Implementation(minSdk = Q, maxSdk = Q)
+    protected void __constructor__(Resources res, int resId) {
+      // In Android Q, this method uses native methods that do not exist in later versions, so
+      // they need to be re-implemented using logic from S.
+      reflector(FontBuilderReflector.class, realFontBuilder).setWeight(-1);
+      reflector(FontBuilderReflector.class, realFontBuilder).setItalic(-1);
+      reflector(FontBuilderReflector.class, realFontBuilder).setLocaleList("");
+      final TypedValue value = new TypedValue();
+      res.getValue(resId, value, true);
+      if (value.string == null) {
+        reflector(FontBuilderReflector.class, realFontBuilder)
+            .setException(new FileNotFoundException(resId + " not found"));
+        return;
+      }
+      final String str = value.string.toString();
+      if (Ascii.toLowerCase(str).endsWith(".xml")) {
+        reflector(FontBuilderReflector.class, realFontBuilder)
+            .setException(new FileNotFoundException(resId + " must be font file."));
+        return;
+      }
+      try {
+        ByteBuffer buf = createBuffer(res.getAssets(), str, false, value.assetCookie);
+        reflector(FontBuilderReflector.class, realFontBuilder).setBuffer(buf);
+      } catch (IOException e) {
+        reflector(FontBuilderReflector.class, realFontBuilder).setException(e);
+      }
+    }
+
+    @Implementation(minSdk = Q)
+    protected static long nInitBuilder() {
+      DefaultNativeRuntimeLoader.injectAndLoad();
+      return FontBuilderNatives.nInitBuilder();
+    }
+
+    @Implementation(minSdk = Q)
+    protected static void nAddAxis(long builderPtr, int tag, float value) {
+      FontBuilderNatives.nAddAxis(builderPtr, tag, value);
+    }
+
+    @Implementation(minSdk = S)
+    protected static long nBuild(
+        long builderPtr,
+        ByteBuffer buffer,
+        String filePath,
+        String localeList,
+        int weight,
+        boolean italic,
+        int ttcIndex) {
+      return FontBuilderNatives.nBuild(
+          builderPtr, buffer, filePath, localeList, weight, italic, ttcIndex);
+    }
+
+    @Implementation(minSdk = Q, maxSdk = R)
+    protected static long nBuild(
+        long builderPtr,
+        ByteBuffer buffer,
+        String filePath,
+        int weight,
+        boolean italic,
+        int ttcIndex) {
+      return nBuild(builderPtr, buffer, filePath, "", weight, italic, ttcIndex);
+    }
+
+    @Implementation(minSdk = Q, maxSdk = TIRAMISU)
+    protected static long nGetReleaseNativeFont() {
+      // Starting in S, nGetReleaseNativeFont was moved from Font.Builder to Font, and despite
+      // existing in S, Font.Builder.nGetReleaseNativeFont does not get registered with a native
+      // method.
+      DefaultNativeRuntimeLoader.injectAndLoad();
+      return FontNatives.nGetReleaseNativeFont();
+    }
+
+    @Implementation(minSdk = S)
+    protected static long nClone(
+        long fontPtr, long builderPtr, int weight, boolean italic, int ttcIndex) {
+      return FontBuilderNatives.nClone(fontPtr, builderPtr, weight, italic, ttcIndex);
+    }
+
+    /**
+     * The Android implementation attempts to call {@link java.nio.ByteBuffer#array()} on a direct
+     * byte buffer. This is supported in Libcore but not the JVM. Use an implementation that copies
+     * the data from the asset into a direct buffer.
+     */
+    @Implementation(minSdk = R)
+    protected static ByteBuffer createBuffer(
+        AssetManager am, String path, boolean isAsset, int cookie) throws IOException {
+      return assetToBuffer(am, path, isAsset, cookie);
+    }
+
+    @ForType(Font.Builder.class)
+    interface FontBuilderReflector {
+      @Accessor("mBuffer")
+      void setBuffer(ByteBuffer buffer);
+
+      @Accessor("mException")
+      void setException(IOException e);
+
+      @Accessor("mWeight")
+      void setWeight(int weight);
+
+      @Accessor("mItalic")
+      void setItalic(int italic);
+
+      @Accessor("mLocaleList")
+      void setLocaleList(String localeList);
+    }
+
+    /** Shadow picker for {@link Font.Builder}. */
+    public static final class Picker extends GraphicsShadowPicker<Object> {
+      public Picker() {
+        super(ShadowFontBuilder.class, ShadowNativeFontBuilder.class);
+      }
+    }
+  }
+
+  static ByteBuffer assetToBuffer(AssetManager am, String path, boolean isAsset, int cookie)
+      throws IOException {
+    Preconditions.checkNotNull(am, "assetManager can not be null");
+    Preconditions.checkNotNull(path, "path can not be null");
+    try (InputStream assetStream =
+        isAsset
+            ? am.open(path, AssetManager.ACCESS_BUFFER)
+            : am.openNonAsset(cookie, path, AssetManager.ACCESS_BUFFER)) {
+      int capacity = assetStream.available();
+      ByteBuffer buffer = ByteBuffer.allocateDirect(capacity);
+      buffer.order(ByteOrder.nativeOrder());
+      byte[] buf = new byte[8 * 1024]; // 8k
+      int bytesRead;
+      while ((bytesRead = assetStream.read(buf)) != -1) {
+        buffer.put(buf, 0, bytesRead);
+      }
+      if (assetStream.read() != -1) {
+        throw new IOException("Unable to access full contents of " + path);
+      }
+      return buffer;
+    }
+  }
+
+  /** Shadow picker for {@link Font}. */
+  public static final class Picker extends GraphicsShadowPicker<Object> {
+    public Picker() {
+      super(ShadowFont.class, ShadowNativeFont.class);
+    }
+  }
+}
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeFontFamily.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeFontFamily.java
new file mode 100644
index 0000000..7a78e47
--- /dev/null
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeFontFamily.java
@@ -0,0 +1,96 @@
+package org.robolectric.shadows;
+
+import static android.os.Build.VERSION_CODES.O;
+import static android.os.Build.VERSION_CODES.O_MR1;
+import static android.os.Build.VERSION_CODES.P;
+import static android.os.Build.VERSION_CODES.Q;
+
+import android.content.res.AssetManager;
+import android.graphics.FontFamily;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import org.robolectric.annotation.Implementation;
+import org.robolectric.annotation.Implements;
+import org.robolectric.nativeruntime.DefaultNativeRuntimeLoader;
+import org.robolectric.nativeruntime.FontFamilyNatives;
+import org.robolectric.shadows.ShadowNativeFontFamily.Picker;
+
+/** Shadow for {@link FontFamily} that is backed by native code */
+@Implements(
+    value = FontFamily.class,
+    minSdk = O,
+    isInAndroidSdk = false,
+    shadowPicker = Picker.class)
+public class ShadowNativeFontFamily {
+  @Implementation(minSdk = O)
+  public static long nInitBuilder(String langs, int variant) {
+    DefaultNativeRuntimeLoader.injectAndLoad();
+    return FontFamilyNatives.nInitBuilder(langs, variant);
+  }
+
+  @Implementation(minSdk = O, maxSdk = O_MR1)
+  protected static void nAllowUnsupportedFont(long builderPtr) {
+    FontFamilyNatives.nAllowUnsupportedFont(builderPtr);
+  }
+
+  @Implementation(minSdk = O)
+  protected static long nCreateFamily(long mBuilderPtr) {
+    return FontFamilyNatives.nCreateFamily(mBuilderPtr);
+  }
+
+  @Implementation(minSdk = P)
+  protected static long nGetBuilderReleaseFunc() {
+    DefaultNativeRuntimeLoader.injectAndLoad();
+    return FontFamilyNatives.nGetBuilderReleaseFunc();
+  }
+
+  @Implementation(minSdk = P)
+  protected static long nGetFamilyReleaseFunc() {
+    return FontFamilyNatives.nGetFamilyReleaseFunc();
+  }
+
+  // By passing -1 to weight argument, the weight value is resolved by OS/2 table in the font.
+  // By passing -1 to italic argument, the italic value is resolved by OS/2 table in the font.
+  @Implementation(minSdk = O)
+  protected static boolean nAddFont(
+      long builderPtr, ByteBuffer font, int ttcIndex, int weight, int isItalic) {
+    return FontFamilyNatives.nAddFont(builderPtr, font, ttcIndex, weight, isItalic);
+  }
+
+  @Implementation(minSdk = O, maxSdk = Q)
+  protected static boolean nAddFontFromAssetManager(
+      long builderPtr,
+      AssetManager mgr,
+      String path,
+      int cookie,
+      boolean isAsset,
+      int ttcIndex,
+      int weight,
+      int isItalic) {
+    try {
+      ByteBuffer byteBuffer = ShadowNativeFont.assetToBuffer(mgr, path, isAsset, cookie);
+      return nAddFont(builderPtr, byteBuffer, ttcIndex, weight, isItalic);
+    } catch (IOException e) {
+      throw new UnsupportedOperationException(e);
+    }
+  }
+
+  @Implementation(minSdk = O)
+  protected static boolean nAddFontWeightStyle(
+      long builderPtr, ByteBuffer font, int ttcIndex, int weight, int isItalic) {
+    return FontFamilyNatives.nAddFontWeightStyle(builderPtr, font, ttcIndex, weight, isItalic);
+  }
+
+  // The added axis values are only valid for the next nAddFont* method call.
+  @Implementation(minSdk = O)
+  protected static void nAddAxisValue(long builderPtr, int tag, float value) {
+    FontFamilyNatives.nAddAxisValue(builderPtr, tag, value);
+  }
+
+  /** Shadow picker for {@link FontFamily}. */
+  public static final class Picker extends GraphicsShadowPicker<Object> {
+    public Picker() {
+      super(ShadowFontFamily.class, ShadowNativeFontFamily.class);
+    }
+  }
+}
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeFontFileUtil.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeFontFileUtil.java
new file mode 100644
index 0000000..a38e280
--- /dev/null
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeFontFileUtil.java
@@ -0,0 +1,45 @@
+package org.robolectric.shadows;
+
+import static android.os.Build.VERSION_CODES.Q;
+import static android.os.Build.VERSION_CODES.S;
+
+import android.graphics.fonts.FontFileUtil;
+import java.nio.ByteBuffer;
+import org.robolectric.annotation.Implementation;
+import org.robolectric.annotation.Implements;
+import org.robolectric.nativeruntime.DefaultNativeRuntimeLoader;
+import org.robolectric.nativeruntime.FontFileUtilNatives;
+import org.robolectric.shadows.ShadowNativeFontFileUtil.Picker;
+
+/** Shadow for {@link FontFileUtil} that is backed by native code */
+@Implements(
+    value = FontFileUtil.class,
+    isInAndroidSdk = false,
+    minSdk = Q,
+    shadowPicker = Picker.class)
+public class ShadowNativeFontFileUtil {
+  @Implementation(minSdk = S)
+  protected static long nGetFontRevision(ByteBuffer buffer, int index) {
+    DefaultNativeRuntimeLoader.injectAndLoad();
+    return FontFileUtilNatives.nGetFontRevision(buffer, index);
+  }
+
+  @Implementation(minSdk = S)
+  protected static String nGetFontPostScriptName(ByteBuffer buffer, int index) {
+    DefaultNativeRuntimeLoader.injectAndLoad();
+    return FontFileUtilNatives.nGetFontPostScriptName(buffer, index);
+  }
+
+  @Implementation(minSdk = S)
+  protected static int nIsPostScriptType1Font(ByteBuffer buffer, int index) {
+    DefaultNativeRuntimeLoader.injectAndLoad();
+    return FontFileUtilNatives.nIsPostScriptType1Font(buffer, index);
+  }
+
+  /** Shadow picker for {@link FontFileUtil}. */
+  public static final class Picker extends GraphicsShadowPicker<Object> {
+    public Picker() {
+      super(null, ShadowNativeFontFileUtil.class);
+    }
+  }
+}
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeFontsFontFamily.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeFontsFontFamily.java
new file mode 100644
index 0000000..360f613
--- /dev/null
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeFontsFontFamily.java
@@ -0,0 +1,86 @@
+package org.robolectric.shadows;
+
+import static android.os.Build.VERSION_CODES.Q;
+import static android.os.Build.VERSION_CODES.S;
+
+import android.graphics.fonts.FontFamily;
+import org.robolectric.annotation.Implementation;
+import org.robolectric.annotation.Implements;
+import org.robolectric.nativeruntime.DefaultNativeRuntimeLoader;
+import org.robolectric.nativeruntime.FontFamilyBuilderNatives;
+import org.robolectric.nativeruntime.FontsFontFamilyNatives;
+import org.robolectric.shadows.ShadowNativeFontsFontFamily.Picker;
+
+/** Shadow for {@link FontFamily} that is backed by native code */
+@Implements(
+    value = FontFamily.class,
+    minSdk = Q,
+    shadowPicker = Picker.class,
+    isInAndroidSdk = false)
+public class ShadowNativeFontsFontFamily {
+  @Implementation(minSdk = S)
+  protected static int nGetFontSize(long family) {
+    return FontsFontFamilyNatives.nGetFontSize(family);
+  }
+
+  @Implementation(minSdk = S)
+  protected static long nGetFont(long family, int i) {
+    return FontsFontFamilyNatives.nGetFont(family, i);
+  }
+
+  @Implementation(minSdk = S)
+  protected static String nGetLangTags(long family) {
+    return FontsFontFamilyNatives.nGetLangTags(family);
+  }
+
+  @Implementation(minSdk = S)
+  protected static int nGetVariant(long family) {
+    return FontsFontFamilyNatives.nGetVariant(family);
+  }
+
+  /** Shadow for {@link FontFamily.Builder} that is backed by native code */
+  @Implements(
+      value = FontFamily.Builder.class,
+      minSdk = Q,
+      shadowPicker = ShadowNativeFontFamilyBuilder.Picker.class,
+      isInAndroidSdk = false)
+  public static class ShadowNativeFontFamilyBuilder {
+    @Implementation
+    protected static long nInitBuilder() {
+      DefaultNativeRuntimeLoader.injectAndLoad();
+      return FontFamilyBuilderNatives.nInitBuilder();
+    }
+
+    @Implementation
+    protected static void nAddFont(long builderPtr, long fontPtr) {
+      FontFamilyBuilderNatives.nAddFont(builderPtr, fontPtr);
+    }
+
+    @Implementation
+    protected static long nBuild(
+        long builderPtr, String langTags, int variant, boolean isCustomFallback) {
+      return FontFamilyBuilderNatives.nBuild(builderPtr, langTags, variant, isCustomFallback);
+    }
+
+    @Implementation
+    protected static long nGetReleaseNativeFamily() {
+      return FontFamilyBuilderNatives.nGetReleaseNativeFamily();
+    }
+
+    /** Shadow picker for {@link FontFamily.Builder}. */
+    public static final class Picker extends GraphicsShadowPicker<Object> {
+      public Picker() {
+        super(
+            ShadowFontsFontFamily.ShadowFontsFontFamilyBuilder.class,
+            ShadowNativeFontFamilyBuilder.class);
+      }
+    }
+  }
+
+  /** Shadow picker for {@link FontFamily}. */
+  public static final class Picker extends GraphicsShadowPicker<Object> {
+    public Picker() {
+      super(ShadowFontsFontFamily.class, ShadowNativeFontsFontFamily.class);
+    }
+  }
+}
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeHardwareRenderer.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeHardwareRenderer.java
new file mode 100644
index 0000000..263522d
--- /dev/null
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeHardwareRenderer.java
@@ -0,0 +1,396 @@
+package org.robolectric.shadows;
+
+import static android.os.Build.VERSION_CODES.Q;
+import static android.os.Build.VERSION_CODES.R;
+import static android.os.Build.VERSION_CODES.S;
+import static android.os.Build.VERSION_CODES.TIRAMISU;
+
+import android.graphics.Bitmap;
+import android.graphics.HardwareRenderer;
+import android.graphics.HardwareRenderer.ASurfaceTransactionCallback;
+import android.graphics.HardwareRenderer.FrameCompleteCallback;
+import android.graphics.HardwareRenderer.FrameDrawingCallback;
+import android.graphics.HardwareRenderer.PictureCapturedCallback;
+import android.graphics.HardwareRenderer.PrepareSurfaceControlForWebviewCallback;
+import android.view.Surface;
+import java.io.FileDescriptor;
+import org.robolectric.annotation.Implementation;
+import org.robolectric.annotation.Implements;
+import org.robolectric.nativeruntime.DefaultNativeRuntimeLoader;
+import org.robolectric.nativeruntime.HardwareRendererNatives;
+import org.robolectric.shadows.ShadowNativeHardwareRenderer.Picker;
+
+/** Shadow for {@link HardwareRenderer} that is backed by native code */
+@Implements(
+    value = HardwareRenderer.class,
+    minSdk = Q,
+    looseSignatures = true,
+    shadowPicker = Picker.class)
+public class ShadowNativeHardwareRenderer {
+  @Implementation
+  protected static void disableVsync() {
+    HardwareRendererNatives.disableVsync();
+  }
+
+  @Implementation
+  protected static void preload() {
+    HardwareRendererNatives.preload();
+  }
+
+  @Implementation(minSdk = S)
+  protected static boolean isWebViewOverlaysEnabled() {
+    return HardwareRendererNatives.isWebViewOverlaysEnabled();
+  }
+
+  @Implementation
+  protected static void setupShadersDiskCache(String cacheFile, String skiaCacheFile) {
+    HardwareRendererNatives.setupShadersDiskCache(cacheFile, skiaCacheFile);
+  }
+
+  @Implementation
+  protected static void nRotateProcessStatsBuffer() {
+    HardwareRendererNatives.nRotateProcessStatsBuffer();
+  }
+
+  @Implementation
+  protected static void nSetProcessStatsBuffer(int fd) {
+    HardwareRendererNatives.nSetProcessStatsBuffer(fd);
+  }
+
+  @Implementation
+  protected static int nGetRenderThreadTid(long nativeProxy) {
+    return HardwareRendererNatives.nGetRenderThreadTid(nativeProxy);
+  }
+
+  @Implementation
+  protected static long nCreateRootRenderNode() {
+    DefaultNativeRuntimeLoader.injectAndLoad();
+    return HardwareRendererNatives.nCreateRootRenderNode();
+  }
+
+  @Implementation(minSdk = S)
+  protected static long nCreateProxy(boolean translucent, long rootRenderNode) {
+    return HardwareRendererNatives.nCreateProxy(translucent, rootRenderNode);
+  }
+
+  @Implementation(minSdk = R, maxSdk = R)
+  protected static long nCreateProxy(
+      boolean translucent, boolean isWideGamut, long rootRenderNode) {
+    return nCreateProxy(true, rootRenderNode);
+  }
+
+  @Implementation(minSdk = Q, maxSdk = Q)
+  protected static Object nCreateProxy(Object translucent, Object rootRenderNode) {
+    return nCreateProxy((boolean) translucent, (long) rootRenderNode);
+  }
+
+  @Implementation
+  protected static void nDeleteProxy(long nativeProxy) {
+    HardwareRendererNatives.nDeleteProxy(nativeProxy);
+  }
+
+  @Implementation
+  protected static boolean nLoadSystemProperties(long nativeProxy) {
+    return HardwareRendererNatives.nLoadSystemProperties(nativeProxy);
+  }
+
+  @Implementation
+  protected static void nSetName(long nativeProxy, String name) {
+    HardwareRendererNatives.nSetName(nativeProxy, name);
+  }
+
+  @Implementation(minSdk = R)
+  protected static void nSetSurface(long nativeProxy, Surface window, boolean discardBuffer) {
+    HardwareRendererNatives.nSetSurface(nativeProxy, window, discardBuffer);
+  }
+
+  @Implementation(minSdk = S)
+  protected static void nSetSurfaceControl(long nativeProxy, long nativeSurfaceControl) {
+    HardwareRendererNatives.nSetSurfaceControl(nativeProxy, nativeSurfaceControl);
+  }
+
+  @Implementation
+  protected static boolean nPause(long nativeProxy) {
+    return HardwareRendererNatives.nPause(nativeProxy);
+  }
+
+  @Implementation
+  protected static void nSetStopped(long nativeProxy, boolean stopped) {
+    HardwareRendererNatives.nSetStopped(nativeProxy, stopped);
+  }
+
+  @Implementation
+  protected static void nSetLightGeometry(
+      long nativeProxy, float lightX, float lightY, float lightZ, float lightRadius) {
+    HardwareRendererNatives.nSetLightGeometry(nativeProxy, lightX, lightY, lightZ, lightRadius);
+  }
+
+  @Implementation
+  protected static void nSetLightAlpha(
+      long nativeProxy, float ambientShadowAlpha, float spotShadowAlpha) {
+    HardwareRendererNatives.nSetLightAlpha(nativeProxy, ambientShadowAlpha, spotShadowAlpha);
+  }
+
+  @Implementation
+  protected static void nSetOpaque(long nativeProxy, boolean opaque) {
+    HardwareRendererNatives.nSetOpaque(nativeProxy, opaque);
+  }
+
+  @Implementation(minSdk = S)
+  protected static void nSetColorMode(long nativeProxy, int colorMode) {
+    HardwareRendererNatives.nSetColorMode(nativeProxy, colorMode);
+  }
+
+  @Implementation(minSdk = S)
+  protected static void nSetSdrWhitePoint(long nativeProxy, float whitePoint) {
+    HardwareRendererNatives.nSetSdrWhitePoint(nativeProxy, whitePoint);
+  }
+
+  @Implementation(minSdk = S)
+  protected static void nSetIsHighEndGfx(boolean isHighEndGfx) {
+    HardwareRendererNatives.nSetIsHighEndGfx(isHighEndGfx);
+  }
+
+  @Implementation
+  protected static int nSyncAndDrawFrame(long nativeProxy, long[] frameInfo, int size) {
+    return HardwareRendererNatives.nSyncAndDrawFrame(nativeProxy, frameInfo, size);
+  }
+
+  @Implementation
+  protected static void nDestroy(long nativeProxy, long rootRenderNode) {
+    HardwareRendererNatives.nDestroy(nativeProxy, rootRenderNode);
+  }
+
+  @Implementation
+  protected static void nRegisterAnimatingRenderNode(long rootRenderNode, long animatingNode) {
+    HardwareRendererNatives.nRegisterAnimatingRenderNode(rootRenderNode, animatingNode);
+  }
+
+  @Implementation
+  protected static void nRegisterVectorDrawableAnimator(long rootRenderNode, long animator) {
+    HardwareRendererNatives.nRegisterVectorDrawableAnimator(rootRenderNode, animator);
+  }
+
+  @Implementation
+  protected static long nCreateTextureLayer(long nativeProxy) {
+    return HardwareRendererNatives.nCreateTextureLayer(nativeProxy);
+  }
+
+  @Implementation
+  protected static void nBuildLayer(long nativeProxy, long node) {
+    HardwareRendererNatives.nBuildLayer(nativeProxy, node);
+  }
+
+  @Implementation
+  protected static boolean nCopyLayerInto(long nativeProxy, long layer, long bitmapHandle) {
+    return HardwareRendererNatives.nCopyLayerInto(nativeProxy, layer, bitmapHandle);
+  }
+
+  @Implementation
+  protected static void nPushLayerUpdate(long nativeProxy, long layer) {
+    HardwareRendererNatives.nPushLayerUpdate(nativeProxy, layer);
+  }
+
+  @Implementation
+  protected static void nCancelLayerUpdate(long nativeProxy, long layer) {
+    HardwareRendererNatives.nCancelLayerUpdate(nativeProxy, layer);
+  }
+
+  @Implementation
+  protected static void nDetachSurfaceTexture(long nativeProxy, long layer) {
+    HardwareRendererNatives.nDetachSurfaceTexture(nativeProxy, layer);
+  }
+
+  @Implementation
+  protected static void nDestroyHardwareResources(long nativeProxy) {
+    HardwareRendererNatives.nDestroyHardwareResources(nativeProxy);
+  }
+
+  @Implementation
+  protected static void nTrimMemory(int level) {
+    HardwareRendererNatives.nTrimMemory(level);
+  }
+
+  @Implementation
+  protected static void nOverrideProperty(String name, String value) {
+    HardwareRendererNatives.nOverrideProperty(name, value);
+  }
+
+  @Implementation
+  protected static void nFence(long nativeProxy) {
+    HardwareRendererNatives.nFence(nativeProxy);
+  }
+
+  @Implementation
+  protected static void nStopDrawing(long nativeProxy) {
+    HardwareRendererNatives.nStopDrawing(nativeProxy);
+  }
+
+  @Implementation
+  protected static void nNotifyFramePending(long nativeProxy) {
+    HardwareRendererNatives.nNotifyFramePending(nativeProxy);
+  }
+
+  @Implementation
+  protected static void nDumpProfileInfo(long nativeProxy, FileDescriptor fd, int dumpFlags) {
+    HardwareRendererNatives.nDumpProfileInfo(nativeProxy, fd, dumpFlags);
+  }
+
+  @Implementation
+  protected static void nAddRenderNode(long nativeProxy, long rootRenderNode, boolean placeFront) {
+    HardwareRendererNatives.nAddRenderNode(nativeProxy, rootRenderNode, placeFront);
+  }
+
+  @Implementation
+  protected static void nRemoveRenderNode(long nativeProxy, long rootRenderNode) {
+    HardwareRendererNatives.nRemoveRenderNode(nativeProxy, rootRenderNode);
+  }
+
+  @Implementation
+  protected static void nDrawRenderNode(long nativeProxy, long rootRenderNode) {
+    HardwareRendererNatives.nDrawRenderNode(nativeProxy, rootRenderNode);
+  }
+
+  @Implementation
+  protected static void nSetContentDrawBounds(
+      long nativeProxy, int left, int top, int right, int bottom) {
+    HardwareRendererNatives.nSetContentDrawBounds(nativeProxy, left, top, right, bottom);
+  }
+
+  @Implementation
+  protected static void nSetPictureCaptureCallback(
+      long nativeProxy, PictureCapturedCallback callback) {
+    HardwareRendererNatives.nSetPictureCaptureCallback(nativeProxy, callback);
+  }
+
+  @Implementation(minSdk = S)
+  protected static void nSetASurfaceTransactionCallback(Object nativeProxy, Object callback) {
+    // Requires looseSignatures because ASurfaceTransactionCallback is S+.
+    HardwareRendererNatives.nSetASurfaceTransactionCallback(
+        (long) nativeProxy, (ASurfaceTransactionCallback) callback);
+  }
+
+  @Implementation(minSdk = S)
+  protected static void nSetPrepareSurfaceControlForWebviewCallback(
+      Object nativeProxy, Object callback) {
+    // Need to use loose signatures here as PrepareSurfaceControlForWebviewCallback is S+.
+    HardwareRendererNatives.nSetPrepareSurfaceControlForWebviewCallback(
+        (long) nativeProxy, (PrepareSurfaceControlForWebviewCallback) callback);
+  }
+
+  @Implementation
+  protected static void nSetFrameCallback(long nativeProxy, FrameDrawingCallback callback) {
+    HardwareRendererNatives.nSetFrameCallback(nativeProxy, callback);
+  }
+
+  @Implementation
+  protected static void nSetFrameCompleteCallback(
+      long nativeProxy, FrameCompleteCallback callback) {
+    HardwareRendererNatives.nSetFrameCompleteCallback(nativeProxy, callback);
+  }
+
+  @Implementation(minSdk = R)
+  protected static void nAddObserver(long nativeProxy, long nativeObserver) {
+    HardwareRendererNatives.nAddObserver(nativeProxy, nativeObserver);
+  }
+
+  @Implementation(minSdk = R)
+  protected static void nRemoveObserver(long nativeProxy, long nativeObserver) {
+    HardwareRendererNatives.nRemoveObserver(nativeProxy, nativeObserver);
+  }
+
+  @Implementation(maxSdk = TIRAMISU)
+  protected static int nCopySurfaceInto(
+      Surface surface, int srcLeft, int srcTop, int srcRight, int srcBottom, long bitmapHandle) {
+    return HardwareRendererNatives.nCopySurfaceInto(
+        surface, srcLeft, srcTop, srcRight, srcBottom, bitmapHandle);
+  }
+
+  @Implementation
+  protected static Bitmap nCreateHardwareBitmap(long renderNode, int width, int height) {
+    return HardwareRendererNatives.nCreateHardwareBitmap(renderNode, width, height);
+  }
+
+  @Implementation
+  protected static void nSetHighContrastText(boolean enabled) {
+    HardwareRendererNatives.nSetHighContrastText(enabled);
+  }
+
+  @Implementation(minSdk = Q, maxSdk = S)
+  protected static void nHackySetRTAnimationsEnabled(boolean enabled) {
+    DefaultNativeRuntimeLoader.injectAndLoad();
+    HardwareRendererNatives.nHackySetRTAnimationsEnabled(enabled);
+  }
+
+  @Implementation
+  protected static void nSetDebuggingEnabled(boolean enabled) {
+    HardwareRendererNatives.nSetDebuggingEnabled(enabled);
+  }
+
+  @Implementation
+  protected static void nSetIsolatedProcess(boolean enabled) {
+    HardwareRendererNatives.nSetIsolatedProcess(enabled);
+  }
+
+  @Implementation
+  protected static void nSetContextPriority(int priority) {
+    HardwareRendererNatives.nSetContextPriority(priority);
+  }
+
+  @Implementation
+  protected static void nAllocateBuffers(long nativeProxy) {
+    HardwareRendererNatives.nAllocateBuffers(nativeProxy);
+  }
+
+  @Implementation
+  protected static void nSetForceDark(long nativeProxy, boolean enabled) {
+    HardwareRendererNatives.nSetForceDark(nativeProxy, enabled);
+  }
+
+  @Implementation(minSdk = S)
+  protected static void nSetDisplayDensityDpi(int densityDpi) {
+    HardwareRendererNatives.nSetDisplayDensityDpi(densityDpi);
+  }
+
+  @Implementation(minSdk = S, maxSdk = TIRAMISU)
+  protected static void nInitDisplayInfo(
+      int width,
+      int height,
+      float refreshRate,
+      int wideColorDataspace,
+      long appVsyncOffsetNanos,
+      long presentationDeadlineNanos) {
+    HardwareRendererNatives.nInitDisplayInfo(
+        width,
+        height,
+        refreshRate,
+        wideColorDataspace,
+        appVsyncOffsetNanos,
+        presentationDeadlineNanos);
+  }
+
+  @Implementation(minSdk = 10000)
+  protected static void nInitDisplayInfo(
+      int width,
+      int height,
+      float refreshRate,
+      int wideColorDataspace,
+      long appVsyncOffsetNanos,
+      long presentationDeadlineNanos,
+      boolean supportsFp16ForHdr) {
+    nInitDisplayInfo(
+        width,
+        height,
+        refreshRate,
+        wideColorDataspace,
+        appVsyncOffsetNanos,
+        presentationDeadlineNanos);
+  }
+
+  /** Shadow picker for {@link HardwareRenderer}. */
+  public static final class Picker extends GraphicsShadowPicker<Object> {
+    public Picker() {
+      super(ShadowHardwareRenderer.class, ShadowNativeHardwareRenderer.class);
+    }
+  }
+}
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeHardwareRendererObserver.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeHardwareRendererObserver.java
new file mode 100644
index 0000000..97b05eb
--- /dev/null
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeHardwareRendererObserver.java
@@ -0,0 +1,60 @@
+package org.robolectric.shadows;
+
+import static android.os.Build.VERSION_CODES.R;
+import static android.os.Build.VERSION_CODES.S;
+import static android.os.Build.VERSION_CODES.S_V2;
+import static android.os.Build.VERSION_CODES.TIRAMISU;
+
+import android.graphics.HardwareRendererObserver;
+import java.lang.ref.WeakReference;
+import org.robolectric.annotation.Implementation;
+import org.robolectric.annotation.Implements;
+import org.robolectric.nativeruntime.DefaultNativeRuntimeLoader;
+import org.robolectric.nativeruntime.HardwareRendererObserverNatives;
+import org.robolectric.shadow.api.Shadow;
+import org.robolectric.shadows.ShadowNativeHardwareRendererObserver.Picker;
+
+/** Shadow for {@link HardwareRendererObserver} that is backed by native code */
+@Implements(
+    value = HardwareRendererObserver.class,
+    minSdk = R,
+    shadowPicker = Picker.class,
+    isInAndroidSdk = false)
+public class ShadowNativeHardwareRendererObserver {
+
+  public HardwareRendererObserverNatives hardwareRendererObserverNatives =
+      new HardwareRendererObserverNatives();
+
+  @Implementation
+  protected static int nGetNextBuffer(long nativePtr, long[] data) {
+    return HardwareRendererObserverNatives.nGetNextBuffer(nativePtr, data);
+  }
+
+  @Implementation(minSdk = R, maxSdk = R)
+  protected long nCreateObserver() {
+    return nCreateObserver(false);
+  }
+
+  @Implementation(minSdk = S, maxSdk = S_V2)
+  protected long nCreateObserver(boolean waitForPresentTime) {
+    DefaultNativeRuntimeLoader.injectAndLoad();
+    return hardwareRendererObserverNatives.nCreateObserver(waitForPresentTime);
+  }
+
+  @Implementation(minSdk = TIRAMISU)
+  protected static long nCreateObserver(
+      WeakReference<HardwareRendererObserver> observer, boolean waitForPresentTime) {
+    HardwareRendererObserver hardwareRendererObserver = observer.get();
+    ShadowNativeHardwareRendererObserver shadowNativeHardwareRendererObserver =
+        Shadow.extract(hardwareRendererObserver);
+    return shadowNativeHardwareRendererObserver.hardwareRendererObserverNatives.nCreateObserver(
+        waitForPresentTime);
+  }
+
+  /** Shadow picker for {@link HardwareRendererObserver}. */
+  public static final class Picker extends GraphicsShadowPicker<Object> {
+    public Picker() {
+      super(null, ShadowNativeHardwareRendererObserver.class);
+    }
+  }
+}
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeImageDecoder.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeImageDecoder.java
new file mode 100644
index 0000000..4913c9f
--- /dev/null
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeImageDecoder.java
@@ -0,0 +1,211 @@
+package org.robolectric.shadows;
+
+import static android.os.Build.VERSION_CODES.P;
+import static android.os.Build.VERSION_CODES.Q;
+import static android.os.Build.VERSION_CODES.R;
+import static android.os.Build.VERSION_CODES.S;
+
+import android.content.res.AssetManager.AssetInputStream;
+import android.graphics.Bitmap;
+import android.graphics.ColorSpace;
+import android.graphics.ImageDecoder;
+import android.graphics.ImageDecoder.Source;
+import android.graphics.Rect;
+import android.util.Size;
+import java.io.FileDescriptor;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.UnsupportedEncodingException;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import org.robolectric.annotation.Implementation;
+import org.robolectric.annotation.Implements;
+import org.robolectric.nativeruntime.DefaultNativeRuntimeLoader;
+import org.robolectric.nativeruntime.ImageDecoderNatives;
+import org.robolectric.shadows.ShadowNativeImageDecoder.Picker;
+
+/** Shadow for {@link android.graphics.ImageDecoder} that is backed by native code */
+@Implements(value = ImageDecoder.class, minSdk = P, shadowPicker = Picker.class)
+public class ShadowNativeImageDecoder {
+
+  static {
+    DefaultNativeRuntimeLoader.injectAndLoad();
+  }
+
+  @Implementation(minSdk = P, maxSdk = Q)
+  protected static ImageDecoder createFromAsset(AssetInputStream ais, Source source)
+      throws IOException {
+    return createFromAsset(ais, false, source);
+  }
+
+  @Implementation(minSdk = R)
+  protected static ImageDecoder createFromAsset(
+      AssetInputStream ais, boolean preferAnimation, Source source) throws IOException {
+    int capacity = ais.available();
+    ByteBuffer buffer = ByteBuffer.allocateDirect(capacity);
+    buffer.order(ByteOrder.nativeOrder());
+    byte[] buf = new byte[8 * 1024]; // 8k
+    int bytesRead;
+    while ((bytesRead = ais.read(buf)) != -1) {
+      buffer.put(buf, 0, bytesRead);
+    }
+    if (ais.read() != -1) {
+      throw new IOException("Unable to access full contents of asset");
+    }
+    return nCreate(buffer, 0, bytesRead, preferAnimation, source);
+  }
+
+  @Implementation(minSdk = P, maxSdk = Q)
+  protected static ImageDecoder nCreate(long asset, Source src) throws IOException {
+    return nCreate(asset, false, src);
+  }
+
+  @Implementation(minSdk = R)
+  protected static ImageDecoder nCreate(long asset, boolean preferAnimation, Source src)
+      throws IOException {
+    throw new UnsupportedEncodingException();
+  }
+
+  @Implementation(minSdk = P, maxSdk = Q)
+  protected static ImageDecoder nCreate(ByteBuffer buffer, int position, int limit, Source src)
+      throws IOException {
+    return nCreate(buffer, position, limit, false, src);
+  }
+
+  @Implementation(minSdk = R)
+  protected static ImageDecoder nCreate(
+      ByteBuffer buffer, int position, int limit, boolean preferAnimation, Source src)
+      throws IOException {
+    return ImageDecoderNatives.nCreate(buffer, position, limit, preferAnimation, src);
+  }
+
+  @Implementation(minSdk = P, maxSdk = Q)
+  protected static ImageDecoder nCreate(byte[] data, int offset, int length, Source src)
+      throws IOException {
+    return nCreate(data, offset, length, false, src);
+  }
+
+  @Implementation(minSdk = R)
+  protected static ImageDecoder nCreate(
+      byte[] data, int offset, int length, boolean preferAnimation, Source src) throws IOException {
+    return ImageDecoderNatives.nCreate(data, offset, length, preferAnimation, src);
+  }
+
+  @Implementation(minSdk = P, maxSdk = Q)
+  protected static ImageDecoder nCreate(InputStream is, byte[] storage, Source src)
+      throws IOException {
+    return nCreate(is, storage, false, src);
+  }
+
+  @Implementation(minSdk = R)
+  protected static ImageDecoder nCreate(
+      InputStream is, byte[] storage, boolean preferAnimation, Source src) throws IOException {
+    return ImageDecoderNatives.nCreate(is, storage, preferAnimation, src);
+  }
+
+  @Implementation(maxSdk = Q)
+  protected static ImageDecoder nCreate(FileDescriptor fd, Source src) throws IOException {
+    throw new UnsupportedEncodingException();
+  }
+
+  @Implementation(minSdk = S)
+  protected static ImageDecoder nCreate(
+      FileDescriptor fd, long length, boolean preferAnimation, Source src) throws IOException {
+    return ImageDecoderNatives.nCreate(fd, length, preferAnimation, src);
+  }
+
+  @Implementation(minSdk = P, maxSdk = P)
+  protected static Bitmap nDecodeBitmap(
+      long nativePtr,
+      ImageDecoder decoder,
+      boolean doPostProcess,
+      int width,
+      int height,
+      Rect cropRect,
+      boolean mutable,
+      int allocator,
+      boolean unpremulRequired,
+      boolean conserveMemory,
+      boolean decodeAsAlphaMask,
+      ColorSpace desiredColorSpace)
+      throws IOException {
+    return nDecodeBitmap(
+        nativePtr,
+        decoder,
+        doPostProcess,
+        width,
+        height,
+        cropRect,
+        mutable,
+        allocator,
+        unpremulRequired,
+        conserveMemory,
+        decodeAsAlphaMask,
+        /* desiredColorSpace = */ 0, // Desired color space is currently not supported in P.
+        /* extended = */ false);
+  }
+
+  @Implementation(minSdk = Q)
+  protected static Bitmap nDecodeBitmap(
+      long nativePtr,
+      ImageDecoder decoder,
+      boolean doPostProcess,
+      int width,
+      int height,
+      Rect cropRect,
+      boolean mutable,
+      int allocator,
+      boolean unpremulRequired,
+      boolean conserveMemory,
+      boolean decodeAsAlphaMask,
+      long desiredColorSpace,
+      boolean extended)
+      throws IOException {
+    return ImageDecoderNatives.nDecodeBitmap(
+        nativePtr,
+        decoder,
+        doPostProcess,
+        width,
+        height,
+        cropRect,
+        mutable,
+        allocator,
+        unpremulRequired,
+        conserveMemory,
+        decodeAsAlphaMask,
+        desiredColorSpace,
+        extended);
+  }
+
+  @Implementation
+  protected static Size nGetSampledSize(long nativePtr, int sampleSize) {
+    return ImageDecoderNatives.nGetSampledSize(nativePtr, sampleSize);
+  }
+
+  @Implementation
+  protected static void nGetPadding(long nativePtr, Rect outRect) {
+    ImageDecoderNatives.nGetPadding(nativePtr, outRect);
+  }
+
+  @Implementation
+  protected static void nClose(long nativePtr) {
+    ImageDecoderNatives.nClose(nativePtr);
+  }
+
+  @Implementation
+  protected static String nGetMimeType(long nativePtr) {
+    return ImageDecoderNatives.nGetMimeType(nativePtr);
+  }
+
+  @Implementation
+  protected static ColorSpace nGetColorSpace(long nativePtr) {
+    return ImageDecoderNatives.nGetColorSpace(nativePtr);
+  }
+
+  /** Shadow picker for {@link ImageDecoder}. */
+  public static final class Picker extends GraphicsShadowPicker<Object> {
+    public Picker() {
+      super(ShadowImageDecoder.class, ShadowNativeImageDecoder.class);
+    }
+  }
+}
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeInterpolator.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeInterpolator.java
new file mode 100644
index 0000000..21a292c
--- /dev/null
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeInterpolator.java
@@ -0,0 +1,55 @@
+package org.robolectric.shadows;
+
+import static android.os.Build.VERSION_CODES.O;
+
+import android.graphics.Interpolator;
+import org.robolectric.annotation.Implementation;
+import org.robolectric.annotation.Implements;
+import org.robolectric.nativeruntime.DefaultNativeRuntimeLoader;
+import org.robolectric.nativeruntime.InterpolatorNatives;
+import org.robolectric.shadows.ShadowNativeInterpolator.Picker;
+
+/** Shadow for {@link Interpolator} that is backed by native code */
+@Implements(value = Interpolator.class, minSdk = O, shadowPicker = Picker.class)
+public class ShadowNativeInterpolator {
+
+  @Implementation
+  protected static long nativeConstructor(int valueCount, int frameCount) {
+    DefaultNativeRuntimeLoader.injectAndLoad();
+    return InterpolatorNatives.nativeConstructor(valueCount, frameCount);
+  }
+
+  @Implementation
+  protected static void nativeDestructor(long nativeInstance) {
+    InterpolatorNatives.nativeDestructor(nativeInstance);
+  }
+
+  @Implementation
+  protected static void nativeReset(long nativeInstance, int valueCount, int frameCount) {
+    InterpolatorNatives.nativeReset(nativeInstance, valueCount, frameCount);
+  }
+
+  @Implementation
+  protected static void nativeSetKeyFrame(
+      long nativeInstance, int index, int msec, float[] values, float[] blend) {
+    InterpolatorNatives.nativeSetKeyFrame(nativeInstance, index, msec, values, blend);
+  }
+
+  @Implementation
+  protected static void nativeSetRepeatMirror(
+      long nativeInstance, float repeatCount, boolean mirror) {
+    InterpolatorNatives.nativeSetRepeatMirror(nativeInstance, repeatCount, mirror);
+  }
+
+  @Implementation
+  protected static int nativeTimeToValues(long nativeInstance, int msec, float[] values) {
+    return InterpolatorNatives.nativeTimeToValues(nativeInstance, msec, values);
+  }
+
+  /** Shadow picker for {@link Interpolator}. */
+  public static final class Picker extends GraphicsShadowPicker<Object> {
+    public Picker() {
+      super(null, ShadowNativeInterpolator.class);
+    }
+  }
+}
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeLightingColorFilter.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeLightingColorFilter.java
new file mode 100644
index 0000000..88e4ea5
--- /dev/null
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeLightingColorFilter.java
@@ -0,0 +1,28 @@
+package org.robolectric.shadows;
+
+import static android.os.Build.VERSION_CODES.O;
+
+import android.graphics.LightingColorFilter;
+import org.robolectric.annotation.Implementation;
+import org.robolectric.annotation.Implements;
+import org.robolectric.nativeruntime.DefaultNativeRuntimeLoader;
+import org.robolectric.nativeruntime.LightingColorFilterNatives;
+import org.robolectric.shadows.ShadowNativeLightingColorFilter.Picker;
+
+/** Shadow for {@link LightingColorFilter} that is backed by native code */
+@Implements(value = LightingColorFilter.class, minSdk = O, shadowPicker = Picker.class)
+public class ShadowNativeLightingColorFilter {
+
+  @Implementation(minSdk = O)
+  protected static long native_CreateLightingFilter(int mul, int add) {
+    DefaultNativeRuntimeLoader.injectAndLoad();
+    return LightingColorFilterNatives.native_CreateLightingFilter(mul, add);
+  }
+
+  /** Shadow picker for {@link LightingColorFilter}. */
+  public static final class Picker extends GraphicsShadowPicker<Object> {
+    public Picker() {
+      super(null, ShadowNativeLightingColorFilter.class);
+    }
+  }
+}
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeLineBreaker.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeLineBreaker.java
new file mode 100644
index 0000000..f5d029c
--- /dev/null
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeLineBreaker.java
@@ -0,0 +1,97 @@
+package org.robolectric.shadows;
+
+import static android.os.Build.VERSION_CODES.Q;
+
+import android.annotation.FloatRange;
+import android.annotation.IntRange;
+import android.graphics.text.LineBreaker;
+import org.robolectric.annotation.Implementation;
+import org.robolectric.annotation.Implements;
+import org.robolectric.nativeruntime.DefaultNativeRuntimeLoader;
+import org.robolectric.nativeruntime.LineBreakerNatives;
+import org.robolectric.shadows.ShadowNativeLineBreaker.Picker;
+
+/** Shadow for {@link LineBreaker} that is backed by native code */
+@Implements(value = LineBreaker.class, minSdk = Q, shadowPicker = Picker.class)
+public class ShadowNativeLineBreaker {
+  @Implementation
+  protected static long nInit(
+      int breakStrategy, int hyphenationFrequency, boolean isJustified, int[] indents) {
+    return LineBreakerNatives.nInit(breakStrategy, hyphenationFrequency, isJustified, indents);
+  }
+
+  @Implementation
+  protected static long nGetReleaseFunc() {
+    // Called first by the static initializer.
+    DefaultNativeRuntimeLoader.injectAndLoad();
+    return LineBreakerNatives.nGetReleaseFunc();
+  }
+
+  @Implementation
+  protected static long nComputeLineBreaks(
+      long nativePtr,
+      char[] text,
+      long measuredTextPtr,
+      @IntRange(from = 0) int length,
+      @FloatRange(from = 0.0f) float firstWidth,
+      @IntRange(from = 0) int firstWidthLineCount,
+      @FloatRange(from = 0.0f) float restWidth,
+      float[] variableTabStops,
+      float defaultTabStop,
+      @IntRange(from = 0) int indentsOffset) {
+    return LineBreakerNatives.nComputeLineBreaks(
+        nativePtr,
+        text,
+        measuredTextPtr,
+        length,
+        firstWidth,
+        firstWidthLineCount,
+        restWidth,
+        variableTabStops,
+        defaultTabStop,
+        indentsOffset);
+  }
+
+  // Result accessors
+  @Implementation
+  protected static int nGetLineCount(long ptr) {
+    return LineBreakerNatives.nGetLineCount(ptr);
+  }
+
+  @Implementation
+  protected static int nGetLineBreakOffset(long ptr, int idx) {
+    return LineBreakerNatives.nGetLineBreakOffset(ptr, idx);
+  }
+
+  @Implementation
+  protected static float nGetLineWidth(long ptr, int idx) {
+    return LineBreakerNatives.nGetLineWidth(ptr, idx);
+  }
+
+  @Implementation
+  protected static float nGetLineAscent(long ptr, int idx) {
+    return LineBreakerNatives.nGetLineAscent(ptr, idx);
+  }
+
+  @Implementation
+  protected static float nGetLineDescent(long ptr, int idx) {
+    return LineBreakerNatives.nGetLineDescent(ptr, idx);
+  }
+
+  @Implementation
+  protected static int nGetLineFlag(long ptr, int idx) {
+    return LineBreakerNatives.nGetLineFlag(ptr, idx);
+  }
+
+  @Implementation
+  protected static long nGetReleaseResultFunc() {
+    return LineBreakerNatives.nGetReleaseResultFunc();
+  }
+
+  /** Shadow picker for {@link LineBreaker}. */
+  public static final class Picker extends GraphicsShadowPicker<Object> {
+    public Picker() {
+      super(ShadowLineBreaker.class, ShadowNativeLineBreaker.class);
+    }
+  }
+}
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeLinearGradient.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeLinearGradient.java
new file mode 100644
index 0000000..c3458fc
--- /dev/null
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeLinearGradient.java
@@ -0,0 +1,60 @@
+package org.robolectric.shadows;
+
+import static android.os.Build.VERSION_CODES.O;
+import static android.os.Build.VERSION_CODES.P;
+import static android.os.Build.VERSION_CODES.Q;
+
+import android.graphics.LinearGradient;
+import org.robolectric.annotation.Implementation;
+import org.robolectric.annotation.Implements;
+import org.robolectric.nativeruntime.DefaultNativeRuntimeLoader;
+import org.robolectric.nativeruntime.LinearGradientNatives;
+import org.robolectric.shadows.ShadowNativeLinearGradient.Picker;
+
+/** Shadow for {@link LinearGradient} that is backed by native code */
+@Implements(value = LinearGradient.class, minSdk = O, shadowPicker = Picker.class)
+public class ShadowNativeLinearGradient {
+  @Implementation(minSdk = Q)
+  protected long nativeCreate(
+      long matrix,
+      float x0,
+      float y0,
+      float x1,
+      float y1,
+      long[] colors,
+      float[] positions,
+      int tileMode,
+      long colorSpaceHandle) {
+    DefaultNativeRuntimeLoader.injectAndLoad();
+    return LinearGradientNatives.nativeCreate(
+        matrix, x0, y0, x1, y1, colors, positions, tileMode, colorSpaceHandle);
+  }
+
+  @Implementation(minSdk = O, maxSdk = P)
+  protected long nativeCreate1(
+      long matrix,
+      float x0,
+      float y0,
+      float x1,
+      float y1,
+      int[] colors,
+      float[] positions,
+      int tileMode) {
+    DefaultNativeRuntimeLoader.injectAndLoad();
+    return LinearGradientNatives.nativeCreate1(matrix, x0, y0, x1, y1, colors, positions, tileMode);
+  }
+
+  @Implementation(minSdk = O, maxSdk = P)
+  protected long nativeCreate2(
+      long matrix, float x0, float y0, float x1, float y1, int color0, int color1, int tileMode) {
+    DefaultNativeRuntimeLoader.injectAndLoad();
+    return LinearGradientNatives.nativeCreate2(matrix, x0, y0, x1, y1, color0, color1, tileMode);
+  }
+
+  /** Shadow picker for {@link LinearGradient}. */
+  public static final class Picker extends GraphicsShadowPicker<Object> {
+    public Picker() {
+      super(null, ShadowNativeLinearGradient.class);
+    }
+  }
+}
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeMaskFilter.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeMaskFilter.java
new file mode 100644
index 0000000..97b18ac
--- /dev/null
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeMaskFilter.java
@@ -0,0 +1,26 @@
+package org.robolectric.shadows;
+
+import static android.os.Build.VERSION_CODES.O;
+
+import android.graphics.MaskFilter;
+import org.robolectric.annotation.Implementation;
+import org.robolectric.annotation.Implements;
+import org.robolectric.nativeruntime.MaskFilterNatives;
+import org.robolectric.shadows.ShadowNativeMaskFilter.Picker;
+
+/** Shadow for {@link MaskFilter} that is backed by native code */
+@Implements(value = MaskFilter.class, minSdk = O, shadowPicker = Picker.class)
+public class ShadowNativeMaskFilter {
+
+  @Implementation(minSdk = O)
+  protected static void nativeDestructor(long nativeFilter) {
+    MaskFilterNatives.nativeDestructor(nativeFilter);
+  }
+
+  /** Shadow picker for {@link MaskFilter}. */
+  public static final class Picker extends GraphicsShadowPicker<Object> {
+    public Picker() {
+      super(null, ShadowNativeMaskFilter.class);
+    }
+  }
+}
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeMatrix.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeMatrix.java
new file mode 100644
index 0000000..e840968
--- /dev/null
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeMatrix.java
@@ -0,0 +1,264 @@
+package org.robolectric.shadows;
+
+import static android.os.Build.VERSION_CODES.LOLLIPOP;
+import static android.os.Build.VERSION_CODES.N_MR1;
+import static android.os.Build.VERSION_CODES.O;
+
+import android.graphics.Matrix;
+import android.graphics.RectF;
+import java.util.List;
+import java.util.Map;
+import org.robolectric.annotation.Implementation;
+import org.robolectric.annotation.Implements;
+import org.robolectric.nativeruntime.DefaultNativeRuntimeLoader;
+import org.robolectric.nativeruntime.MatrixNatives;
+
+/** Shadow for {@link Matrix} that is backed by native code */
+@Implements(value = Matrix.class, minSdk = O, isInAndroidSdk = false)
+public class ShadowNativeMatrix extends ShadowMatrix {
+
+  @Implementation(minSdk = LOLLIPOP, maxSdk = N_MR1)
+  protected static long native_create(long nSrcOrZero) {
+    return nCreate(nSrcOrZero);
+  }
+
+  @Implementation(minSdk = O)
+  protected static long nCreate(long nSrcOrZero) {
+    DefaultNativeRuntimeLoader.injectAndLoad();
+    return MatrixNatives.nCreate(nSrcOrZero);
+  }
+
+  @Implementation(minSdk = O)
+  protected static long nGetNativeFinalizer() {
+    return MatrixNatives.nGetNativeFinalizer();
+  }
+
+  @Implementation(minSdk = O)
+  protected static boolean nSetRectToRect(long nObject, RectF src, RectF dst, int stf) {
+    return MatrixNatives.nSetRectToRect(nObject, src, dst, stf);
+  }
+
+  @Implementation(minSdk = O)
+  protected static boolean nSetPolyToPoly(
+      long nObject, float[] src, int srcIndex, float[] dst, int dstIndex, int pointCount) {
+    return MatrixNatives.nSetPolyToPoly(nObject, src, srcIndex, dst, dstIndex, pointCount);
+  }
+
+  @Implementation(minSdk = O)
+  protected static void nMapPoints(
+      long nObject,
+      float[] dst,
+      int dstIndex,
+      float[] src,
+      int srcIndex,
+      int ptCount,
+      boolean isPts) {
+    MatrixNatives.nMapPoints(nObject, dst, dstIndex, src, srcIndex, ptCount, isPts);
+  }
+
+  @Implementation(minSdk = O)
+  protected static boolean nMapRect(long nObject, RectF dst, RectF src) {
+    return MatrixNatives.nMapRect(nObject, dst, src);
+  }
+
+  @Implementation(minSdk = O)
+  protected static void nGetValues(long nObject, float[] values) {
+    MatrixNatives.nGetValues(nObject, values);
+  }
+
+  @Implementation(minSdk = O)
+  protected static void nSetValues(long nObject, float[] values) {
+    MatrixNatives.nSetValues(nObject, values);
+  }
+
+  @Implementation(minSdk = O)
+  protected static boolean nIsIdentity(long nObject) {
+    return MatrixNatives.nIsIdentity(nObject);
+  }
+
+  @Implementation(minSdk = O)
+  protected static boolean nIsAffine(long nObject) {
+    return MatrixNatives.nIsAffine(nObject);
+  }
+
+  @Implementation(minSdk = O)
+  protected static boolean nRectStaysRect(long nObject) {
+    return MatrixNatives.nRectStaysRect(nObject);
+  }
+
+  @Implementation(minSdk = O)
+  protected static void nReset(long nObject) {
+    MatrixNatives.nReset(nObject);
+  }
+
+  @Implementation(minSdk = O)
+  protected static void nSet(long nObject, long nOther) {
+    MatrixNatives.nSet(nObject, nOther);
+  }
+
+  @Implementation(minSdk = O)
+  protected static void nSetTranslate(long nObject, float dx, float dy) {
+    MatrixNatives.nSetTranslate(nObject, dx, dy);
+  }
+
+  @Implementation(minSdk = O)
+  protected static void nSetScale(long nObject, float sx, float sy, float px, float py) {
+    MatrixNatives.nSetScale(nObject, sx, sy, px, py);
+  }
+
+  @Implementation(minSdk = O)
+  protected static void nSetScale(long nObject, float sx, float sy) {
+    MatrixNatives.nSetScale(nObject, sx, sy);
+  }
+
+  @Implementation(minSdk = O)
+  protected static void nSetRotate(long nObject, float degrees, float px, float py) {
+    MatrixNatives.nSetRotate(nObject, degrees, px, py);
+  }
+
+  @Implementation(minSdk = O)
+  protected static void nSetRotate(long nObject, float degrees) {
+    MatrixNatives.nSetRotate(nObject, degrees);
+  }
+
+  @Implementation(minSdk = O)
+  protected static void nSetSinCos(
+      long nObject, float sinValue, float cosValue, float px, float py) {
+    MatrixNatives.nSetSinCos(nObject, sinValue, cosValue, px, py);
+  }
+
+  @Implementation(minSdk = O)
+  protected static void nSetSinCos(long nObject, float sinValue, float cosValue) {
+    MatrixNatives.nSetSinCos(nObject, sinValue, cosValue);
+  }
+
+  @Implementation(minSdk = O)
+  protected static void nSetSkew(long nObject, float kx, float ky, float px, float py) {
+    MatrixNatives.nSetSkew(nObject, kx, ky, px, py);
+  }
+
+  @Implementation(minSdk = O)
+  protected static void nSetSkew(long nObject, float kx, float ky) {
+    MatrixNatives.nSetSkew(nObject, kx, ky);
+  }
+
+  @Implementation(minSdk = O)
+  protected static void nSetConcat(long nObject, long nA, long nB) {
+    MatrixNatives.nSetConcat(nObject, nA, nB);
+  }
+
+  @Implementation(minSdk = O)
+  protected static void nPreTranslate(long nObject, float dx, float dy) {
+    MatrixNatives.nPreTranslate(nObject, dx, dy);
+  }
+
+  @Implementation(minSdk = O)
+  protected static void nPreScale(long nObject, float sx, float sy, float px, float py) {
+    MatrixNatives.nPreScale(nObject, sx, sy, px, py);
+  }
+
+  @Implementation(minSdk = O)
+  protected static void nPreScale(long nObject, float sx, float sy) {
+    MatrixNatives.nPreScale(nObject, sx, sy);
+  }
+
+  @Implementation(minSdk = O)
+  protected static void nPreRotate(long nObject, float degrees, float px, float py) {
+    MatrixNatives.nPreRotate(nObject, degrees, px, py);
+  }
+
+  @Implementation(minSdk = O)
+  protected static void nPreRotate(long nObject, float degrees) {
+    MatrixNatives.nPreRotate(nObject, degrees);
+  }
+
+  @Implementation(minSdk = O)
+  protected static void nPreSkew(long nObject, float kx, float ky, float px, float py) {
+    MatrixNatives.nPreSkew(nObject, kx, ky, px, py);
+  }
+
+  @Implementation(minSdk = O)
+  protected static void nPreSkew(long nObject, float kx, float ky) {
+    MatrixNatives.nPreSkew(nObject, kx, ky);
+  }
+
+  @Implementation(minSdk = O)
+  protected static void nPreConcat(long nObject, long nOtherMatrix) {
+    MatrixNatives.nPreConcat(nObject, nOtherMatrix);
+  }
+
+  @Implementation(minSdk = O)
+  protected static void nPostTranslate(long nObject, float dx, float dy) {
+    MatrixNatives.nPostTranslate(nObject, dx, dy);
+  }
+
+  @Implementation(minSdk = O)
+  protected static void nPostScale(long nObject, float sx, float sy, float px, float py) {
+    MatrixNatives.nPostScale(nObject, sx, sy, px, py);
+  }
+
+  @Implementation(minSdk = O)
+  protected static void nPostScale(long nObject, float sx, float sy) {
+    MatrixNatives.nPostScale(nObject, sx, sy);
+  }
+
+  @Implementation(minSdk = O)
+  protected static void nPostRotate(long nObject, float degrees, float px, float py) {
+    MatrixNatives.nPostRotate(nObject, degrees, px, py);
+  }
+
+  @Implementation(minSdk = O)
+  protected static void nPostRotate(long nObject, float degrees) {
+    MatrixNatives.nPostRotate(nObject, degrees);
+  }
+
+  @Implementation(minSdk = O)
+  protected static void nPostSkew(long nObject, float kx, float ky, float px, float py) {
+    MatrixNatives.nPostSkew(nObject, kx, ky, px, py);
+  }
+
+  @Implementation(minSdk = O)
+  protected static void nPostSkew(long nObject, float kx, float ky) {
+    MatrixNatives.nPostSkew(nObject, kx, ky);
+  }
+
+  @Implementation(minSdk = O)
+  protected static void nPostConcat(long nObject, long nOtherMatrix) {
+    MatrixNatives.nPostConcat(nObject, nOtherMatrix);
+  }
+
+  @Implementation(minSdk = O)
+  protected static boolean nInvert(long nObject, long nInverse) {
+    return MatrixNatives.nInvert(nObject, nInverse);
+  }
+
+  @Implementation(minSdk = O)
+  protected static float nMapRadius(long nObject, float radius) {
+    return MatrixNatives.nMapRadius(nObject, radius);
+  }
+
+  @Implementation(minSdk = O)
+  protected static boolean nEquals(long nA, long nB) {
+    return MatrixNatives.nEquals(nA, nB);
+  }
+
+  @Override
+  public List<String> getPreOperations() {
+    throw new UnsupportedOperationException("Legacy ShadowMatrix APIs are not supported");
+  }
+
+  @Override
+  public List<String> getPostOperations() {
+    throw new UnsupportedOperationException("Legacy ShadowMatrix APIs are not supported");
+  }
+
+  @Override
+  public Map<String, String> getSetOperations() {
+    throw new UnsupportedOperationException("Legacy ShadowMatrix APIs are not supported");
+  }
+
+  @Override
+  public String getDescription() {
+    throw new UnsupportedOperationException("Legacy ShadowMatrix APIs are not supported");
+  }
+}
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeMeasuredParagraph.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeMeasuredParagraph.java
new file mode 100644
index 0000000..130cd7b
--- /dev/null
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeMeasuredParagraph.java
@@ -0,0 +1,73 @@
+package org.robolectric.shadows;
+
+import static android.os.Build.VERSION_CODES.P;
+
+import android.graphics.Rect;
+import android.text.MeasuredParagraph;
+import org.robolectric.annotation.Implementation;
+import org.robolectric.annotation.Implements;
+import org.robolectric.nativeruntime.DefaultNativeRuntimeLoader;
+import org.robolectric.nativeruntime.MeasuredTextBuilderNatives;
+import org.robolectric.nativeruntime.MeasuredTextNatives;
+import org.robolectric.shadows.ShadowNativeMeasuredParagraph.Picker;
+
+/** Shadow for {@link MeasuredParagraph} that is backed by native code */
+@Implements(value = MeasuredParagraph.class, minSdk = P, maxSdk = P, shadowPicker = Picker.class)
+public class ShadowNativeMeasuredParagraph {
+  @Implementation
+  protected static long nInitBuilder() {
+    DefaultNativeRuntimeLoader.injectAndLoad();
+    return MeasuredTextBuilderNatives.nInitBuilder();
+  }
+
+  @Implementation
+  protected static void nAddStyleRun(
+      long nativeBuilderPtr, long paintPtr, int start, int end, boolean isRtl) {
+    MeasuredTextBuilderNatives.nAddStyleRun(nativeBuilderPtr, paintPtr, start, end, isRtl);
+  }
+
+  @Implementation
+  protected static void nAddReplacementRun(
+      long nativeBuilderPtr, long paintPtr, int start, int end, float width) {
+    MeasuredTextBuilderNatives.nAddReplacementRun(nativeBuilderPtr, paintPtr, start, end, width);
+  }
+
+  @Implementation
+  protected static long nBuildNativeMeasuredParagraph(
+      long nativeBuilderPtr, char[] text, boolean computeHyphenation, boolean computeLayout) {
+    return MeasuredTextBuilderNatives.nBuildMeasuredText(
+        nativeBuilderPtr, 0, text, computeHyphenation, computeLayout);
+  }
+
+  @Implementation
+  protected static void nFreeBuilder(long nativeBuilderPtr) {
+    MeasuredTextBuilderNatives.nFreeBuilder(nativeBuilderPtr);
+  }
+
+  @Implementation
+  protected static float nGetWidth(long nativePtr, int start, int end) {
+    return MeasuredTextNatives.nGetWidth(nativePtr, start, end);
+  }
+
+  @Implementation
+  protected static long nGetReleaseFunc() {
+    return MeasuredTextNatives.nGetReleaseFunc();
+  }
+
+  @Implementation
+  protected static int nGetMemoryUsage(long nativePtr) {
+    return MeasuredTextNatives.nGetMemoryUsage(nativePtr);
+  }
+
+  @Implementation
+  protected static void nGetBounds(long nativePtr, char[] buf, int start, int end, Rect rect) {
+    MeasuredTextNatives.nGetBounds(nativePtr, buf, start, end, rect);
+  }
+
+  /** Shadow picker for {@link MeasuredParagraph}. */
+  public static final class Picker extends GraphicsShadowPicker<Object> {
+    public Picker() {
+      super(ShadowMeasuredParagraph.class, ShadowNativeMeasuredParagraph.class);
+    }
+  }
+}
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeMeasuredText.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeMeasuredText.java
new file mode 100644
index 0000000..5b82a6c
--- /dev/null
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeMeasuredText.java
@@ -0,0 +1,135 @@
+package org.robolectric.shadows;
+
+import static android.os.Build.VERSION_CODES.Q;
+import static android.os.Build.VERSION_CODES.S_V2;
+import static android.os.Build.VERSION_CODES.TIRAMISU;
+
+import android.annotation.FloatRange;
+import android.annotation.IntRange;
+import android.graphics.Rect;
+import android.graphics.text.MeasuredText;
+import org.robolectric.annotation.Implementation;
+import org.robolectric.annotation.Implements;
+import org.robolectric.nativeruntime.DefaultNativeRuntimeLoader;
+import org.robolectric.nativeruntime.MeasuredTextBuilderNatives;
+import org.robolectric.nativeruntime.MeasuredTextNatives;
+import org.robolectric.shadows.ShadowNativeMeasuredText.Picker;
+
+/** Shadow for {@link MeasuredText} that is backed by native code */
+@Implements(value = MeasuredText.class, minSdk = Q, shadowPicker = Picker.class)
+public class ShadowNativeMeasuredText {
+  @Implementation
+  protected static float nGetWidth(
+      /* Non Zero */ long nativePtr, @IntRange(from = 0) int start, @IntRange(from = 0) int end) {
+    return MeasuredTextNatives.nGetWidth(nativePtr, start, end);
+  }
+
+  @Implementation
+  protected static /* Non Zero */ long nGetReleaseFunc() {
+    DefaultNativeRuntimeLoader.injectAndLoad();
+    return MeasuredTextNatives.nGetReleaseFunc();
+  }
+
+  @Implementation
+  protected static int nGetMemoryUsage(/* Non Zero */ long nativePtr) {
+    return MeasuredTextNatives.nGetMemoryUsage(nativePtr);
+  }
+
+  @Implementation
+  protected static void nGetBounds(long nativePtr, char[] buf, int start, int end, Rect rect) {
+    MeasuredTextNatives.nGetBounds(nativePtr, buf, start, end, rect);
+  }
+
+  @Implementation
+  protected static float nGetCharWidthAt(long nativePtr, int offset) {
+    return MeasuredTextNatives.nGetCharWidthAt(nativePtr, offset);
+  }
+
+  /** Shadow for {@link MeasuredText.Builder} that is backed by native code */
+  @Implements(
+      value = MeasuredText.Builder.class,
+      minSdk = Q,
+      shadowPicker = ShadowNativeMeasuredTextBuilder.Picker.class)
+  public static class ShadowNativeMeasuredTextBuilder {
+    @Implementation
+    protected static /* Non Zero */ long nInitBuilder() {
+      return MeasuredTextBuilderNatives.nInitBuilder();
+    }
+
+    @Implementation(maxSdk = S_V2)
+    protected static void nAddStyleRun(
+        /* Non Zero */ long nativeBuilderPtr,
+        /* Non Zero */ long paintPtr,
+        @IntRange(from = 0) int start,
+        @IntRange(from = 0) int end,
+        boolean isRtl) {
+      MeasuredTextBuilderNatives.nAddStyleRun(nativeBuilderPtr, paintPtr, start, end, isRtl);
+    }
+
+    @Implementation(minSdk = TIRAMISU)
+    protected static void nAddStyleRun(
+        /* Non Zero */ long nativeBuilderPtr,
+        /* Non Zero */ long paintPtr,
+        int lineBreakStyle,
+        int lineBreakWordStyle,
+        int start,
+        int end,
+        boolean isRtl) {
+      MeasuredTextBuilderNatives.nAddStyleRun(nativeBuilderPtr, paintPtr, start, end, isRtl);
+    }
+
+    @Implementation
+    protected static void nAddReplacementRun(
+        /* Non Zero */ long nativeBuilderPtr,
+        /* Non Zero */ long paintPtr,
+        @IntRange(from = 0) int start,
+        @IntRange(from = 0) int end,
+        @FloatRange(from = 0) float width) {
+      MeasuredTextBuilderNatives.nAddReplacementRun(nativeBuilderPtr, paintPtr, start, end, width);
+    }
+
+    @Implementation(maxSdk = S_V2)
+    protected static long nBuildMeasuredText(
+        /* Non Zero */ long nativeBuilderPtr,
+        long hintMtPtr,
+        char[] text,
+        boolean computeHyphenation,
+        boolean computeLayout) {
+      return MeasuredTextBuilderNatives.nBuildMeasuredText(
+          nativeBuilderPtr, hintMtPtr, text, computeHyphenation, computeLayout);
+    }
+
+    @Implementation(minSdk = TIRAMISU)
+    protected static long nBuildMeasuredText(
+        /* Non Zero */ long nativeBuilderPtr,
+        long hintMtPtr,
+        char[] text,
+        boolean computeHyphenation,
+        boolean computeLayout,
+        boolean fastHyphenationMode) {
+      return MeasuredTextBuilderNatives.nBuildMeasuredText(
+          nativeBuilderPtr, hintMtPtr, text, computeHyphenation, computeLayout);
+    }
+
+    @Implementation
+    protected static void nFreeBuilder(/* Non Zero */ long nativeBuilderPtr) {
+      MeasuredTextBuilderNatives.nFreeBuilder(nativeBuilderPtr);
+    }
+
+    /** Shadow picker for {@link MeasuredText.Builder}. */
+    public static final class Picker extends GraphicsShadowPicker<Object> {
+      public Picker() {
+        super(
+            org.robolectric.shadows.ShadowMeasuredTextBuilder.class,
+            ShadowNativeMeasuredText.ShadowNativeMeasuredTextBuilder.class);
+      }
+    }
+  }
+
+  /** Shadow picker for {@link MeasuredText}. */
+  public static final class Picker extends GraphicsShadowPicker<Object> {
+    public Picker() {
+      super(null, ShadowNativeMeasuredText.class);
+    }
+  }
+}
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeNativeInterpolatorFactory.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeNativeInterpolatorFactory.java
new file mode 100644
index 0000000..21c80e5
--- /dev/null
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeNativeInterpolatorFactory.java
@@ -0,0 +1,85 @@
+package org.robolectric.shadows;
+
+import static android.os.Build.VERSION_CODES.R;
+
+import android.graphics.animation.NativeInterpolatorFactory;
+import org.robolectric.annotation.Implementation;
+import org.robolectric.annotation.Implements;
+import org.robolectric.nativeruntime.DefaultNativeRuntimeLoader;
+import org.robolectric.nativeruntime.NativeInterpolatorFactoryNatives;
+import org.robolectric.shadows.ShadowNativeNativeInterpolatorFactory.Picker;
+
+/** Shadow for {@link NativeInterpolatorFactory} that is backed by native code */
+@Implements(
+    value = NativeInterpolatorFactory.class,
+    minSdk = R,
+    shadowPicker = Picker.class,
+    isInAndroidSdk = false)
+public class ShadowNativeNativeInterpolatorFactory {
+
+  static {
+    DefaultNativeRuntimeLoader.injectAndLoad();
+  }
+
+  @Implementation
+  protected static long createAccelerateDecelerateInterpolator() {
+    return NativeInterpolatorFactoryNatives.createAccelerateDecelerateInterpolator();
+  }
+
+  @Implementation
+  protected static long createAccelerateInterpolator(float factor) {
+    return NativeInterpolatorFactoryNatives.createAccelerateInterpolator(factor);
+  }
+
+  @Implementation
+  protected static long createAnticipateInterpolator(float tension) {
+    return NativeInterpolatorFactoryNatives.createAnticipateInterpolator(tension);
+  }
+
+  @Implementation
+  protected static long createAnticipateOvershootInterpolator(float tension) {
+    return NativeInterpolatorFactoryNatives.createAnticipateOvershootInterpolator(tension);
+  }
+
+  @Implementation
+  protected static long createBounceInterpolator() {
+    return NativeInterpolatorFactoryNatives.createBounceInterpolator();
+  }
+
+  @Implementation
+  protected static long createCycleInterpolator(float cycles) {
+    return NativeInterpolatorFactoryNatives.createCycleInterpolator(cycles);
+  }
+
+  @Implementation
+  protected static long createDecelerateInterpolator(float factor) {
+    return NativeInterpolatorFactoryNatives.createDecelerateInterpolator(factor);
+  }
+
+  @Implementation
+  protected static long createLinearInterpolator() {
+    return NativeInterpolatorFactoryNatives.createLinearInterpolator();
+  }
+
+  @Implementation
+  protected static long createOvershootInterpolator(float tension) {
+    return NativeInterpolatorFactoryNatives.createOvershootInterpolator(tension);
+  }
+
+  @Implementation
+  protected static long createPathInterpolator(float[] x, float[] y) {
+    return NativeInterpolatorFactoryNatives.createPathInterpolator(x, y);
+  }
+
+  @Implementation
+  protected static long createLutInterpolator(float[] values) {
+    return NativeInterpolatorFactoryNatives.createLutInterpolator(values);
+  }
+
+  /** Shadow picker for {@link NativeInterpolatorFactory}. */
+  public static final class Picker extends GraphicsShadowPicker<Object> {
+    public Picker() {
+      super(null, ShadowNativeNativeInterpolatorFactory.class);
+    }
+  }
+}
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeNinePatch.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeNinePatch.java
new file mode 100644
index 0000000..11e5ba8
--- /dev/null
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeNinePatch.java
@@ -0,0 +1,50 @@
+package org.robolectric.shadows;
+
+import static android.os.Build.VERSION_CODES.O;
+import static android.os.Build.VERSION_CODES.Q;
+
+import android.graphics.NinePatch;
+import android.graphics.Rect;
+import org.robolectric.annotation.Implementation;
+import org.robolectric.annotation.Implements;
+import org.robolectric.nativeruntime.DefaultNativeRuntimeLoader;
+import org.robolectric.nativeruntime.NinePatchNatives;
+import org.robolectric.shadows.ShadowNativeNinePatch.Picker;
+
+/** Shadow for {@link NinePatch} that is backed by native code */
+@Implements(
+    value = NinePatch.class,
+    minSdk = O,
+    shadowPicker = Picker.class,
+    isInAndroidSdk = false)
+public class ShadowNativeNinePatch {
+
+  @Implementation
+  protected static boolean isNinePatchChunk(byte[] chunk) {
+    DefaultNativeRuntimeLoader.injectAndLoad();
+    return NinePatchNatives.isNinePatchChunk(chunk);
+  }
+
+  @Implementation
+  protected static long validateNinePatchChunk(byte[] chunk) {
+    DefaultNativeRuntimeLoader.injectAndLoad();
+    return NinePatchNatives.validateNinePatchChunk(chunk);
+  }
+
+  @Implementation
+  protected static void nativeFinalize(long chunk) {
+    NinePatchNatives.nativeFinalize(chunk);
+  }
+
+  @Implementation(minSdk = Q)
+  protected static long nativeGetTransparentRegion(long bitmapHandle, long chunk, Rect location) {
+    return NinePatchNatives.nativeGetTransparentRegion(bitmapHandle, chunk, location);
+  }
+
+  /** Shadow picker for {@link NinePatch}. */
+  public static final class Picker extends GraphicsShadowPicker<Object> {
+    public Picker() {
+      super(ShadowNinePatch.class, ShadowNativeNinePatch.class);
+    }
+  }
+}
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativePaint.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativePaint.java
new file mode 100644
index 0000000..94fadb5
--- /dev/null
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativePaint.java
@@ -0,0 +1,847 @@
+package org.robolectric.shadows;
+
+import static android.os.Build.VERSION_CODES.O;
+import static android.os.Build.VERSION_CODES.O_MR1;
+import static android.os.Build.VERSION_CODES.P;
+import static android.os.Build.VERSION_CODES.Q;
+import static android.os.Build.VERSION_CODES.TIRAMISU;
+
+import android.graphics.Paint;
+import android.graphics.Paint.FontMetrics;
+import android.graphics.Paint.FontMetricsInt;
+import android.graphics.Rect;
+import androidx.annotation.ColorInt;
+import androidx.annotation.ColorLong;
+import org.robolectric.annotation.Implementation;
+import org.robolectric.annotation.Implements;
+import org.robolectric.nativeruntime.DefaultNativeRuntimeLoader;
+import org.robolectric.nativeruntime.PaintNatives;
+import org.robolectric.shadows.ShadowNativePaint.Picker;
+
+/** Shadow for {@link Paint} that is backed by native code */
+@Implements(
+    minSdk = O,
+    value = Paint.class,
+    looseSignatures = true,
+    shadowPicker = Picker.class,
+    isInAndroidSdk = false)
+public class ShadowNativePaint {
+
+  // nGetTextRunCursor methods are non-static
+  private PaintNatives paintNatives = new PaintNatives();
+
+  @Implementation(minSdk = O)
+  protected static long nGetNativeFinalizer() {
+    return PaintNatives.nGetNativeFinalizer();
+  }
+
+  @Implementation(minSdk = O)
+  protected static long nInit() {
+    DefaultNativeRuntimeLoader.injectAndLoad();
+    // This native code calls Typeface::resolveDefault, which requires Typeface clinit to have run.
+    ShadowNativeTypeface.ensureInitialized();
+    return PaintNatives.nInit();
+  }
+
+  @Implementation(minSdk = O, maxSdk = P)
+  protected static int nGetHyphenEdit(long paintPtr) {
+    return PaintNatives.nGetEndHyphenEdit(paintPtr);
+  }
+
+  @Implementation(minSdk = O, maxSdk = P)
+  protected static void nSetHyphenEdit(long paintPtr, int hyphen) {
+    PaintNatives.nSetStartHyphenEdit(paintPtr, 0);
+    PaintNatives.nSetEndHyphenEdit(paintPtr, hyphen);
+  }
+
+  @Implementation(minSdk = O)
+  protected static long nInitWithPaint(long paint) {
+    DefaultNativeRuntimeLoader.injectAndLoad();
+    return PaintNatives.nInitWithPaint(paint);
+  }
+
+  @Implementation(minSdk = P)
+  protected static int nBreakText(
+      long nObject,
+      char[] text,
+      int index,
+      int count,
+      float maxWidth,
+      int bidiFlags,
+      float[] measuredWidth) {
+    return PaintNatives.nBreakText(nObject, text, index, count, maxWidth, bidiFlags, measuredWidth);
+  }
+
+  @Implementation(minSdk = P)
+  protected static int nBreakText(
+      long nObject,
+      String text,
+      boolean measureForwards,
+      float maxWidth,
+      int bidiFlags,
+      float[] measuredWidth) {
+    return PaintNatives.nBreakText(
+        nObject, text, measureForwards, maxWidth, bidiFlags, measuredWidth);
+  }
+
+  @Implementation(minSdk = O, maxSdk = O_MR1)
+  protected static int nBreakText(
+      long nObject,
+      long typefacePtr,
+      char[] text,
+      int index,
+      int count,
+      float maxWidth,
+      int bidiFlags,
+      float[] measuredWidth) {
+    return PaintNatives.nBreakText(
+        nObject, typefacePtr, text, index, count, maxWidth, bidiFlags, measuredWidth);
+  }
+
+  @Implementation(minSdk = O, maxSdk = O_MR1)
+  protected static int nBreakText(
+      long nObject,
+      long typefacePtr,
+      String text,
+      boolean measureForwards,
+      float maxWidth,
+      int bidiFlags,
+      float[] measuredWidth) {
+    return PaintNatives.nBreakText(
+        nObject, typefacePtr, text, measureForwards, maxWidth, bidiFlags, measuredWidth);
+  }
+
+  @Implementation(minSdk = P)
+  protected static float nGetTextAdvances(
+      long paintPtr,
+      char[] text,
+      int index,
+      int count,
+      int contextIndex,
+      int contextCount,
+      int bidiFlags,
+      float[] advances,
+      int advancesIndex) {
+    return PaintNatives.nGetTextAdvances(
+        paintPtr,
+        text,
+        index,
+        count,
+        contextIndex,
+        contextCount,
+        bidiFlags,
+        advances,
+        advancesIndex);
+  }
+
+  @Implementation(minSdk = P)
+  protected static float nGetTextAdvances(
+      long paintPtr,
+      String text,
+      int start,
+      int end,
+      int contextStart,
+      int contextEnd,
+      int bidiFlags,
+      float[] advances,
+      int advancesIndex) {
+    return PaintNatives.nGetTextAdvances(
+        paintPtr, text, start, end, contextStart, contextEnd, bidiFlags, advances, advancesIndex);
+  }
+
+  @Implementation(minSdk = O, maxSdk = O_MR1)
+  protected static float nGetTextAdvances(
+      long paintPtr,
+      long typefacePtr,
+      char[] text,
+      int index,
+      int count,
+      int contextIndex,
+      int contextCount,
+      int bidiFlags,
+      float[] advances,
+      int advancesIndex) {
+    return PaintNatives.nGetTextAdvances(
+        paintPtr,
+        typefacePtr,
+        text,
+        index,
+        count,
+        contextIndex,
+        contextCount,
+        bidiFlags,
+        advances,
+        advancesIndex);
+  }
+
+  @Implementation(minSdk = O, maxSdk = O_MR1)
+  protected static float nGetTextAdvances(
+      long paintPtr,
+      long typefacePtr,
+      String text,
+      int index,
+      int count,
+      int contextIndex,
+      int contextCount,
+      int bidiFlags,
+      float[] advances,
+      int advancesIndex) {
+    return PaintNatives.nGetTextAdvances(
+        paintPtr,
+        typefacePtr,
+        text,
+        index,
+        count,
+        contextIndex,
+        contextCount,
+        bidiFlags,
+        advances,
+        advancesIndex);
+  }
+
+  @Implementation(minSdk = P)
+  protected int nGetTextRunCursor(
+      long paintPtr,
+      char[] text,
+      int contextStart,
+      int contextLength,
+      int dir,
+      int offset,
+      int cursorOpt) {
+    return paintNatives.nGetTextRunCursor(
+        paintPtr, text, contextStart, contextLength, dir, offset, cursorOpt);
+  }
+
+  @Implementation(minSdk = P)
+  protected int nGetTextRunCursor(
+      long paintPtr,
+      String text,
+      int contextStart,
+      int contextEnd,
+      int dir,
+      int offset,
+      int cursorOpt) {
+    return paintNatives.nGetTextRunCursor(
+        paintPtr, text, contextStart, contextEnd, dir, offset, cursorOpt);
+  }
+
+  @Implementation(minSdk = O, maxSdk = O_MR1)
+  protected int nGetTextRunCursor(
+      long paintPtr,
+      long typefacePtr,
+      char[] text,
+      int contextStart,
+      int contextLength,
+      int dir,
+      int offset,
+      int cursorOpt) {
+    return paintNatives.nGetTextRunCursor(
+        paintPtr, typefacePtr, text, contextStart, contextLength, dir, offset, cursorOpt);
+  }
+
+  @Implementation(minSdk = O, maxSdk = O_MR1)
+  protected int nGetTextRunCursor(
+      long paintPtr,
+      long typefacePtr,
+      String text,
+      int contextStart,
+      int contextEnd,
+      int dir,
+      int offset,
+      int cursorOpt) {
+    return paintNatives.nGetTextRunCursor(
+        paintPtr, typefacePtr, text, contextStart, contextEnd, dir, offset, cursorOpt);
+  }
+
+  @Implementation(minSdk = P)
+  protected static void nGetTextPath(
+      long paintPtr,
+      int bidiFlags,
+      char[] text,
+      int index,
+      int count,
+      float x,
+      float y,
+      long path) {
+    PaintNatives.nGetTextPath(paintPtr, bidiFlags, text, index, count, x, y, path);
+  }
+
+  @Implementation(minSdk = P)
+  protected static void nGetTextPath(
+      long paintPtr, int bidiFlags, String text, int start, int end, float x, float y, long path) {
+    PaintNatives.nGetTextPath(paintPtr, bidiFlags, text, start, end, x, y, path);
+  }
+
+  @Implementation(minSdk = O, maxSdk = O_MR1)
+  protected static void nGetTextPath(
+      long paintPtr,
+      long typefacePtr,
+      int bidiFlags,
+      char[] text,
+      int index,
+      int count,
+      float x,
+      float y,
+      long path) {
+    PaintNatives.nGetTextPath(paintPtr, typefacePtr, bidiFlags, text, index, count, x, y, path);
+  }
+
+  @Implementation(minSdk = O, maxSdk = O_MR1)
+  protected static void nGetTextPath(
+      long paintPtr,
+      long typefacePtr,
+      int bidiFlags,
+      String text,
+      int start,
+      int end,
+      float x,
+      float y,
+      long path) {
+    PaintNatives.nGetTextPath(paintPtr, typefacePtr, bidiFlags, text, start, end, x, y, path);
+  }
+
+  @Implementation(minSdk = P)
+  protected static void nGetStringBounds(
+      long nativePaint, String text, int start, int end, int bidiFlags, Rect bounds) {
+    PaintNatives.nGetStringBounds(nativePaint, text, start, end, bidiFlags, bounds);
+  }
+
+  @Implementation(minSdk = O, maxSdk = O_MR1)
+  protected static void nGetStringBounds(
+      long nativePaint,
+      long typefacePtr,
+      String text,
+      int start,
+      int end,
+      int bidiFlags,
+      Rect bounds) {
+    PaintNatives.nGetStringBounds(nativePaint, typefacePtr, text, start, end, bidiFlags, bounds);
+  }
+
+  @Implementation(minSdk = O, maxSdk = P)
+  protected static int nGetColor(long paintPtr) {
+    return PaintNatives.nGetColor(paintPtr);
+  }
+
+  @Implementation(minSdk = O, maxSdk = P)
+  protected static int nGetAlpha(long paintPtr) {
+    return PaintNatives.nGetAlpha(paintPtr);
+  }
+
+  @Implementation(minSdk = P)
+  protected static void nGetCharArrayBounds(
+      long nativePaint, char[] text, int index, int count, int bidiFlags, Rect bounds) {
+    PaintNatives.nGetCharArrayBounds(nativePaint, text, index, count, bidiFlags, bounds);
+  }
+
+  @Implementation(minSdk = O, maxSdk = O_MR1)
+  protected static void nGetCharArrayBounds(
+      long nativePaint,
+      long typefacePtr,
+      char[] text,
+      int index,
+      int count,
+      int bidiFlags,
+      Rect bounds) {
+    PaintNatives.nGetCharArrayBounds(
+        nativePaint, typefacePtr, text, index, count, bidiFlags, bounds);
+  }
+
+  @Implementation(minSdk = P)
+  protected static boolean nHasGlyph(long paintPtr, int bidiFlags, String string) {
+    return PaintNatives.nHasGlyph(paintPtr, bidiFlags, string);
+  }
+
+  @Implementation(minSdk = O, maxSdk = O_MR1)
+  protected static boolean nHasGlyph(
+      long paintPtr, long typefacePtr, int bidiFlags, String string) {
+    return PaintNatives.nHasGlyph(paintPtr, typefacePtr, bidiFlags, string);
+  }
+
+  @Implementation(minSdk = P)
+  protected static float nGetRunAdvance(
+      long paintPtr,
+      char[] text,
+      int start,
+      int end,
+      int contextStart,
+      int contextEnd,
+      boolean isRtl,
+      int offset) {
+    return PaintNatives.nGetRunAdvance(
+        paintPtr, text, start, end, contextStart, contextEnd, isRtl, offset);
+  }
+
+  @Implementation(minSdk = O, maxSdk = O_MR1)
+  protected static float nGetRunAdvance(
+      long paintPtr,
+      long typefacePtr,
+      char[] text,
+      int start,
+      int end,
+      int contextStart,
+      int contextEnd,
+      boolean isRtl,
+      int offset) {
+    return PaintNatives.nGetRunAdvance(
+        paintPtr, text, start, end, contextStart, contextEnd, isRtl, offset);
+  }
+
+  @Implementation(minSdk = O, maxSdk = O_MR1)
+  protected static int nGetOffsetForAdvance(
+      long paintPtr,
+      long typefacePtr,
+      char[] text,
+      int start,
+      int end,
+      int contextStart,
+      int contextEnd,
+      boolean isRtl,
+      float advance) {
+    return PaintNatives.nGetOffsetForAdvance(
+        paintPtr, typefacePtr, text, start, end, contextStart, contextEnd, isRtl, advance);
+  }
+
+  @Implementation(minSdk = P)
+  protected static int nGetOffsetForAdvance(
+      long paintPtr,
+      char[] text,
+      int start,
+      int end,
+      int contextStart,
+      int contextEnd,
+      boolean isRtl,
+      float advance) {
+    return PaintNatives.nGetOffsetForAdvance(
+        paintPtr, text, start, end, contextStart, contextEnd, isRtl, advance);
+  }
+
+  @Implementation(minSdk = O)
+  protected static int nSetTextLocales(long paintPtr, String locales) {
+    return PaintNatives.nSetTextLocales(paintPtr, locales);
+  }
+
+  @Implementation(minSdk = O)
+  protected static void nSetFontFeatureSettings(long paintPtr, String settings) {
+    PaintNatives.nSetFontFeatureSettings(paintPtr, settings);
+  }
+
+  @Implementation(minSdk = P)
+  protected static float nGetFontMetrics(long paintPtr, FontMetrics metrics) {
+    return PaintNatives.nGetFontMetrics(paintPtr, metrics);
+  }
+
+  @Implementation(minSdk = O, maxSdk = O_MR1)
+  protected static float nGetFontMetrics(long paintPtr, long typefacePtr, FontMetrics metrics) {
+    return PaintNatives.nGetFontMetrics(paintPtr, typefacePtr, metrics);
+  }
+
+  @Implementation(minSdk = P)
+  protected static int nGetFontMetricsInt(long paintPtr, FontMetricsInt fmi) {
+    return PaintNatives.nGetFontMetricsInt(paintPtr, fmi);
+  }
+
+  @Implementation(minSdk = O, maxSdk = O_MR1)
+  protected static int nGetFontMetricsInt(long paintPtr, long typefacePtr, FontMetricsInt fmi) {
+    return PaintNatives.nGetFontMetricsInt(paintPtr, typefacePtr, fmi);
+  }
+
+  @Implementation(minSdk = O)
+  protected static void nReset(long paintPtr) {
+    PaintNatives.nReset(paintPtr);
+  }
+
+  @Implementation(minSdk = O)
+  protected static void nSet(long paintPtrDest, long paintPtrSrc) {
+    PaintNatives.nSet(paintPtrDest, paintPtrSrc);
+  }
+
+  @Implementation(minSdk = O)
+  protected static int nGetStyle(long paintPtr) {
+    return PaintNatives.nGetStyle(paintPtr);
+  }
+
+  @Implementation(minSdk = O)
+  protected static void nSetStyle(long paintPtr, int style) {
+    PaintNatives.nSetStyle(paintPtr, style);
+  }
+
+  @Implementation(minSdk = O)
+  protected static int nGetStrokeCap(long paintPtr) {
+    return PaintNatives.nGetStrokeCap(paintPtr);
+  }
+
+  @Implementation(minSdk = O)
+  protected static void nSetStrokeCap(long paintPtr, int cap) {
+    PaintNatives.nSetStrokeCap(paintPtr, cap);
+  }
+
+  @Implementation(minSdk = O)
+  protected static int nGetStrokeJoin(long paintPtr) {
+    return PaintNatives.nGetStrokeJoin(paintPtr);
+  }
+
+  @Implementation(minSdk = O)
+  protected static void nSetStrokeJoin(long paintPtr, int join) {
+    PaintNatives.nSetStrokeJoin(paintPtr, join);
+  }
+
+  @Implementation(minSdk = O)
+  protected static boolean nGetFillPath(long paintPtr, long src, long dst) {
+    return PaintNatives.nGetFillPath(paintPtr, src, dst);
+  }
+
+  @Implementation(minSdk = O)
+  protected static long nSetShader(long paintPtr, long shader) {
+    return PaintNatives.nSetShader(paintPtr, shader);
+  }
+
+  @Implementation(minSdk = O)
+  protected static long nSetColorFilter(long paintPtr, long filter) {
+    return PaintNatives.nSetColorFilter(paintPtr, filter);
+  }
+
+  @Implementation(minSdk = O)
+  protected static void nSetXfermode(long paintPtr, int xfermode) {
+    PaintNatives.nSetXfermode(paintPtr, xfermode);
+  }
+
+  @Implementation(minSdk = O)
+  protected static long nSetPathEffect(long paintPtr, long effect) {
+    return PaintNatives.nSetPathEffect(paintPtr, effect);
+  }
+
+  @Implementation(minSdk = O)
+  protected static long nSetMaskFilter(long paintPtr, long maskfilter) {
+    return PaintNatives.nSetMaskFilter(paintPtr, maskfilter);
+  }
+
+  @Implementation(minSdk = P)
+  protected static void nSetTypeface(long paintPtr, long typeface) {
+    PaintNatives.nSetTypeface(paintPtr, typeface);
+  }
+
+  @Implementation(minSdk = O, maxSdk = O_MR1)
+  protected static Object nSetTypeface(Object paintPtr, Object typeface) {
+    PaintNatives.nSetTypeface((long) paintPtr, (long) typeface);
+    return paintPtr;
+  }
+
+  @Implementation(minSdk = O)
+  protected static int nGetTextAlign(long paintPtr) {
+    return PaintNatives.nGetTextAlign(paintPtr);
+  }
+
+  @Implementation(minSdk = O)
+  protected static void nSetTextAlign(long paintPtr, int align) {
+    PaintNatives.nSetTextAlign(paintPtr, align);
+  }
+
+  @Implementation(minSdk = P)
+  protected static void nSetTextLocalesByMinikinLocaleListId(
+      long paintPtr, int mMinikinLocaleListId) {
+    PaintNatives.nSetTextLocalesByMinikinLocaleListId(paintPtr, mMinikinLocaleListId);
+  }
+
+  @Implementation(minSdk = Q)
+  protected static void nSetShadowLayer(
+      long paintPtr,
+      float radius,
+      float dx,
+      float dy,
+      long colorSpaceHandle,
+      @ColorLong long shadowColor) {
+    PaintNatives.nSetShadowLayer(paintPtr, radius, dx, dy, colorSpaceHandle, shadowColor);
+  }
+
+  @Implementation(minSdk = O, maxSdk = P)
+  protected static void nSetShadowLayer(
+      long paintPtr, float radius, float dx, float dy, int color) {
+    PaintNatives.nSetShadowLayer(paintPtr, radius, dx, dy, color);
+  }
+
+  @Implementation(minSdk = O)
+  protected static boolean nHasShadowLayer(long paintPtr) {
+    return PaintNatives.nHasShadowLayer(paintPtr);
+  }
+
+  @Implementation(minSdk = O)
+  protected static float nGetLetterSpacing(long paintPtr) {
+    return PaintNatives.nGetLetterSpacing(paintPtr);
+  }
+
+  @Implementation(minSdk = O)
+  protected static void nSetLetterSpacing(long paintPtr, float letterSpacing) {
+    PaintNatives.nSetLetterSpacing(paintPtr, letterSpacing);
+  }
+
+  @Implementation(minSdk = O)
+  protected static float nGetWordSpacing(long paintPtr) {
+    return PaintNatives.nGetWordSpacing(paintPtr);
+  }
+
+  @Implementation(minSdk = O)
+  protected static void nSetWordSpacing(long paintPtr, float wordSpacing) {
+    PaintNatives.nSetWordSpacing(paintPtr, wordSpacing);
+  }
+
+  @Implementation(minSdk = Q)
+  protected static int nGetStartHyphenEdit(long paintPtr) {
+    return PaintNatives.nGetStartHyphenEdit(paintPtr);
+  }
+
+  @Implementation(minSdk = Q)
+  protected static int nGetEndHyphenEdit(long paintPtr) {
+    return PaintNatives.nGetEndHyphenEdit(paintPtr);
+  }
+
+  @Implementation(minSdk = Q)
+  protected static void nSetStartHyphenEdit(long paintPtr, int hyphen) {
+    PaintNatives.nSetStartHyphenEdit(paintPtr, hyphen);
+  }
+
+  @Implementation(minSdk = Q)
+  protected static void nSetEndHyphenEdit(long paintPtr, int hyphen) {
+    PaintNatives.nSetEndHyphenEdit(paintPtr, hyphen);
+  }
+
+  @Implementation(minSdk = O)
+  protected static void nSetStrokeMiter(long paintPtr, float miter) {
+    PaintNatives.nSetStrokeMiter(paintPtr, miter);
+  }
+
+  @Implementation(minSdk = O)
+  protected static float nGetStrokeMiter(long paintPtr) {
+    return PaintNatives.nGetStrokeMiter(paintPtr);
+  }
+
+  @Implementation(minSdk = O)
+  protected static void nSetStrokeWidth(long paintPtr, float width) {
+    PaintNatives.nSetStrokeWidth(paintPtr, width);
+  }
+
+  @Implementation(minSdk = O)
+  protected static float nGetStrokeWidth(long paintPtr) {
+    return PaintNatives.nGetStrokeWidth(paintPtr);
+  }
+
+  @Implementation(minSdk = O)
+  protected static void nSetAlpha(long paintPtr, int a) {
+    PaintNatives.nSetAlpha(paintPtr, a);
+  }
+
+  @Implementation(minSdk = O)
+  protected static void nSetDither(long paintPtr, boolean dither) {
+    PaintNatives.nSetDither(paintPtr, dither);
+  }
+
+  @Implementation(minSdk = O)
+  protected static int nGetFlags(long paintPtr) {
+    return PaintNatives.nGetFlags(paintPtr);
+  }
+
+  @Implementation(minSdk = O)
+  protected static void nSetFlags(long paintPtr, int flags) {
+    PaintNatives.nSetFlags(paintPtr, flags);
+  }
+
+  @Implementation(minSdk = O)
+  protected static int nGetHinting(long paintPtr) {
+    return PaintNatives.nGetHinting(paintPtr);
+  }
+
+  @Implementation(minSdk = O)
+  protected static void nSetHinting(long paintPtr, int mode) {
+    PaintNatives.nSetHinting(paintPtr, mode);
+  }
+
+  @Implementation(minSdk = O)
+  protected static void nSetAntiAlias(long paintPtr, boolean aa) {
+    PaintNatives.nSetAntiAlias(paintPtr, aa);
+  }
+
+  @Implementation(minSdk = O)
+  protected static void nSetLinearText(long paintPtr, boolean linearText) {
+    PaintNatives.nSetLinearText(paintPtr, linearText);
+  }
+
+  @Implementation(minSdk = O)
+  protected static void nSetSubpixelText(long paintPtr, boolean subpixelText) {
+    PaintNatives.nSetSubpixelText(paintPtr, subpixelText);
+  }
+
+  @Implementation(minSdk = O)
+  protected static void nSetUnderlineText(long paintPtr, boolean underlineText) {
+    PaintNatives.nSetUnderlineText(paintPtr, underlineText);
+  }
+
+  @Implementation(minSdk = O)
+  protected static void nSetFakeBoldText(long paintPtr, boolean fakeBoldText) {
+    PaintNatives.nSetFakeBoldText(paintPtr, fakeBoldText);
+  }
+
+  @Implementation(minSdk = O)
+  protected static void nSetFilterBitmap(long paintPtr, boolean filter) {
+    PaintNatives.nSetFilterBitmap(paintPtr, filter);
+  }
+
+  @Implementation(minSdk = Q)
+  protected static void nSetColor(long paintPtr, long colorSpaceHandle, @ColorLong long color) {
+    PaintNatives.nSetColor(paintPtr, colorSpaceHandle, color);
+  }
+
+  @Implementation(minSdk = O)
+  protected static void nSetColor(long paintPtr, @ColorInt int color) {
+    PaintNatives.nSetColor(paintPtr, color);
+  }
+
+  @Implementation(minSdk = O)
+  protected static void nSetStrikeThruText(long paintPtr, boolean strikeThruText) {
+    PaintNatives.nSetStrikeThruText(paintPtr, strikeThruText);
+  }
+
+  @Implementation(minSdk = O)
+  protected static boolean nIsElegantTextHeight(long paintPtr) {
+    return PaintNatives.nIsElegantTextHeight(paintPtr);
+  }
+
+  @Implementation(minSdk = O)
+  protected static void nSetElegantTextHeight(long paintPtr, boolean elegant) {
+    PaintNatives.nSetElegantTextHeight(paintPtr, elegant);
+  }
+
+  @Implementation(minSdk = O)
+  protected static float nGetTextSize(long paintPtr) {
+    return PaintNatives.nGetTextSize(paintPtr);
+  }
+
+  @Implementation(minSdk = O)
+  protected static float nGetTextScaleX(long paintPtr) {
+    return PaintNatives.nGetTextScaleX(paintPtr);
+  }
+
+  @Implementation(minSdk = O)
+  protected static void nSetTextScaleX(long paintPtr, float scaleX) {
+    PaintNatives.nSetTextScaleX(paintPtr, scaleX);
+  }
+
+  @Implementation(minSdk = O)
+  protected static float nGetTextSkewX(long paintPtr) {
+    return PaintNatives.nGetTextSkewX(paintPtr);
+  }
+
+  @Implementation(minSdk = O)
+  protected static void nSetTextSkewX(long paintPtr, float skewX) {
+    PaintNatives.nSetTextSkewX(paintPtr, skewX);
+  }
+
+  @Implementation(minSdk = P)
+  protected static float nAscent(long paintPtr) {
+    return PaintNatives.nAscent(paintPtr);
+  }
+
+  @Implementation(minSdk = O, maxSdk = O_MR1)
+  protected static float nAscent(long paintPtr, long typefacePtr) {
+    return PaintNatives.nAscent(paintPtr, typefacePtr);
+  }
+
+  @Implementation(minSdk = P)
+  protected static float nDescent(long paintPtr) {
+    return PaintNatives.nDescent(paintPtr);
+  }
+
+  @Implementation(minSdk = O, maxSdk = O_MR1)
+  protected static float nDescent(long paintPtr, long typefacePtr) {
+    return PaintNatives.nDescent(paintPtr, typefacePtr);
+  }
+
+  @Implementation(minSdk = P)
+  protected static float nGetUnderlinePosition(long paintPtr) {
+    return PaintNatives.nGetUnderlinePosition(paintPtr);
+  }
+
+  @Implementation(minSdk = P)
+  protected static float nGetUnderlineThickness(long paintPtr) {
+    return PaintNatives.nGetUnderlineThickness(paintPtr);
+  }
+
+  @Implementation(minSdk = P)
+  protected static float nGetStrikeThruPosition(long paintPtr) {
+    return PaintNatives.nGetStrikeThruPosition(paintPtr);
+  }
+
+  @Implementation(minSdk = P)
+  protected static float nGetStrikeThruThickness(long paintPtr) {
+    return PaintNatives.nGetStrikeThruThickness(paintPtr);
+  }
+
+  @Implementation(minSdk = O)
+  protected static void nSetTextSize(long paintPtr, float textSize) {
+    PaintNatives.nSetTextSize(paintPtr, textSize);
+  }
+
+  @Implementation(minSdk = P)
+  protected static boolean nEqualsForTextMeasurement(long leftPaintPtr, long rightPaintPtr) {
+    return PaintNatives.nEqualsForTextMeasurement(leftPaintPtr, rightPaintPtr);
+  }
+
+  @Implementation(minSdk = TIRAMISU)
+  protected static void nGetFontMetricsIntForText(
+      long paintPtr,
+      char[] text,
+      int start,
+      int count,
+      int ctxStart,
+      int ctxCount,
+      boolean isRtl,
+      FontMetricsInt outMetrics) {
+    PaintNatives.nGetFontMetricsIntForText(
+        paintPtr, text, start, count, ctxStart, ctxCount, isRtl, outMetrics);
+  }
+
+  @Implementation(minSdk = TIRAMISU)
+  protected static void nGetFontMetricsIntForText(
+      long paintPtr,
+      String text,
+      int start,
+      int count,
+      int ctxStart,
+      int ctxCount,
+      boolean isRtl,
+      FontMetricsInt outMetrics) {
+    PaintNatives.nGetFontMetricsIntForText(
+        paintPtr, text, start, count, ctxStart, ctxCount, isRtl, outMetrics);
+  }
+
+  @Implementation(minSdk = 10000)
+  protected static float nGetRunCharacterAdvance(
+      long paintPtr,
+      char[] text,
+      int start,
+      int end,
+      int contextStart,
+      int contextEnd,
+      boolean isRtl,
+      int offset,
+      float[] advances,
+      int advancesIndex) {
+    return PaintNatives.nGetRunCharacterAdvance(
+        paintPtr,
+        text,
+        start,
+        end,
+        contextStart,
+        contextEnd,
+        isRtl,
+        offset,
+        advances,
+        advancesIndex);
+  }
+
+  /** Shadow picker for {@link Paint}. */
+  public static final class Picker extends GraphicsShadowPicker<Object> {
+    public Picker() {
+      super(ShadowPaint.class, ShadowNativePaint.class);
+    }
+  }
+}
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativePath.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativePath.java
new file mode 100644
index 0000000..c162df5
--- /dev/null
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativePath.java
@@ -0,0 +1,243 @@
+package org.robolectric.shadows;
+
+import static android.os.Build.VERSION_CODES.O;
+import static android.os.Build.VERSION_CODES.P;
+
+import android.graphics.Path;
+import android.graphics.RectF;
+import java.util.List;
+import org.robolectric.annotation.Implementation;
+import org.robolectric.annotation.Implements;
+import org.robolectric.nativeruntime.DefaultNativeRuntimeLoader;
+import org.robolectric.nativeruntime.PathNatives;
+
+/** Shadow for {@link Path} that is backed by native code */
+@Implements(value = Path.class, minSdk = O, isInAndroidSdk = false)
+public class ShadowNativePath extends ShadowPath {
+
+  @Implementation(minSdk = O)
+  protected static long nInit() {
+    DefaultNativeRuntimeLoader.injectAndLoad();
+    return PathNatives.nInit();
+  }
+
+  @Implementation(minSdk = O)
+  protected static long nInit(long nPath) {
+    // Required for pre-P.
+    DefaultNativeRuntimeLoader.injectAndLoad();
+    return PathNatives.nInit(nPath);
+  }
+
+  @Implementation(minSdk = P)
+  protected static long nGetFinalizer() {
+    // Required for pre-P.
+    DefaultNativeRuntimeLoader.injectAndLoad();
+    return PathNatives.nGetFinalizer();
+  }
+
+  @Implementation(minSdk = O)
+  protected static void nSet(long nativeDst, long nSrc) {
+    PathNatives.nSet(nativeDst, nSrc);
+  }
+
+  @Implementation(minSdk = O)
+  protected static void nComputeBounds(long nPath, RectF bounds) {
+    PathNatives.nComputeBounds(nPath, bounds);
+  }
+
+  @Implementation(minSdk = O)
+  protected static void nIncReserve(long nPath, int extraPtCount) {
+    PathNatives.nIncReserve(nPath, extraPtCount);
+  }
+
+  @Implementation(minSdk = O)
+  protected static void nMoveTo(long nPath, float x, float y) {
+    PathNatives.nMoveTo(nPath, x, y);
+  }
+
+  @Implementation(minSdk = O)
+  protected static void nRMoveTo(long nPath, float dx, float dy) {
+    PathNatives.nRMoveTo(nPath, dx, dy);
+  }
+
+  @Implementation(minSdk = O)
+  protected static void nLineTo(long nPath, float x, float y) {
+    PathNatives.nLineTo(nPath, x, y);
+  }
+
+  @Implementation(minSdk = O)
+  protected static void nRLineTo(long nPath, float dx, float dy) {
+    PathNatives.nRLineTo(nPath, dx, dy);
+  }
+
+  @Implementation(minSdk = O)
+  protected static void nQuadTo(long nPath, float x1, float y1, float x2, float y2) {
+    PathNatives.nQuadTo(nPath, x1, y1, x2, y2);
+  }
+
+  @Implementation(minSdk = O)
+  protected static void nRQuadTo(long nPath, float dx1, float dy1, float dx2, float dy2) {
+    PathNatives.nRQuadTo(nPath, dx1, dy1, dx2, dy2);
+  }
+
+  @Implementation(minSdk = O)
+  protected static void nCubicTo(
+      long nPath, float x1, float y1, float x2, float y2, float x3, float y3) {
+    PathNatives.nCubicTo(nPath, x1, y1, x2, y2, x3, y3);
+  }
+
+  @Implementation(minSdk = O)
+  protected static void nRCubicTo(
+      long nPath, float x1, float y1, float x2, float y2, float x3, float y3) {
+    PathNatives.nRCubicTo(nPath, x1, y1, x2, y2, x3, y3);
+  }
+
+  @Implementation(minSdk = O)
+  protected static void nArcTo(
+      long nPath,
+      float left,
+      float top,
+      float right,
+      float bottom,
+      float startAngle,
+      float sweepAngle,
+      boolean forceMoveTo) {
+    PathNatives.nArcTo(nPath, left, top, right, bottom, startAngle, sweepAngle, forceMoveTo);
+  }
+
+  @Implementation(minSdk = O)
+  protected static void nClose(long nPath) {
+    PathNatives.nClose(nPath);
+  }
+
+  @Implementation(minSdk = O)
+  protected static void nAddRect(
+      long nPath, float left, float top, float right, float bottom, int dir) {
+    PathNatives.nAddRect(nPath, left, top, right, bottom, dir);
+  }
+
+  @Implementation(minSdk = O)
+  protected static void nAddOval(
+      long nPath, float left, float top, float right, float bottom, int dir) {
+    PathNatives.nAddOval(nPath, left, top, right, bottom, dir);
+  }
+
+  @Implementation(minSdk = O)
+  protected static void nAddCircle(long nPath, float x, float y, float radius, int dir) {
+    PathNatives.nAddCircle(nPath, x, y, radius, dir);
+  }
+
+  @Implementation(minSdk = O)
+  protected static void nAddArc(
+      long nPath,
+      float left,
+      float top,
+      float right,
+      float bottom,
+      float startAngle,
+      float sweepAngle) {
+    PathNatives.nAddArc(nPath, left, top, right, bottom, startAngle, sweepAngle);
+  }
+
+  @Implementation(minSdk = O)
+  protected static void nAddRoundRect(
+      long nPath, float left, float top, float right, float bottom, float rx, float ry, int dir) {
+    PathNatives.nAddRoundRect(nPath, left, top, right, bottom, rx, ry, dir);
+  }
+
+  @Implementation(minSdk = O)
+  protected static void nAddRoundRect(
+      long nPath, float left, float top, float right, float bottom, float[] radii, int dir) {
+    PathNatives.nAddRoundRect(nPath, left, top, right, bottom, radii, dir);
+  }
+
+  @Implementation(minSdk = O)
+  protected static void nAddPath(long nPath, long src, float dx, float dy) {
+    PathNatives.nAddPath(nPath, src, dx, dy);
+  }
+
+  @Implementation(minSdk = O)
+  protected static void nAddPath(long nPath, long src) {
+    PathNatives.nAddPath(nPath, src);
+  }
+
+  @Implementation(minSdk = O)
+  protected static void nAddPath(long nPath, long src, long matrix) {
+    PathNatives.nAddPath(nPath, src, matrix);
+  }
+
+  @Implementation(minSdk = O)
+  protected static void nOffset(long nPath, float dx, float dy) {
+    PathNatives.nOffset(nPath, dx, dy);
+  }
+
+  @Implementation(minSdk = O)
+  protected static void nSetLastPoint(long nPath, float dx, float dy) {
+    PathNatives.nSetLastPoint(nPath, dx, dy);
+  }
+
+  @Implementation(minSdk = O)
+  protected static void nTransform(long nPath, long matrix, long dstPath) {
+    PathNatives.nTransform(nPath, matrix, dstPath);
+  }
+
+  @Implementation(minSdk = O)
+  protected static void nTransform(long nPath, long matrix) {
+    PathNatives.nTransform(nPath, matrix);
+  }
+
+  @Implementation(minSdk = O)
+  protected static boolean nOp(long path1, long path2, int op, long result) {
+    return PathNatives.nOp(path1, path2, op, result);
+  }
+
+  @Implementation(minSdk = O)
+  protected static boolean nIsRect(long nPath, RectF rect) {
+    return PathNatives.nIsRect(nPath, rect);
+  }
+
+  @Implementation(minSdk = O)
+  protected static void nReset(long nPath) {
+    PathNatives.nReset(nPath);
+  }
+
+  @Implementation(minSdk = O)
+  protected static void nRewind(long nPath) {
+    PathNatives.nRewind(nPath);
+  }
+
+  @Implementation(minSdk = O)
+  protected static boolean nIsEmpty(long nPath) {
+    return PathNatives.nIsEmpty(nPath);
+  }
+
+  @Implementation(minSdk = O)
+  protected static boolean nIsConvex(long nPath) {
+    return PathNatives.nIsConvex(nPath);
+  }
+
+  @Implementation(minSdk = O)
+  protected static int nGetFillType(long nPath) {
+    return PathNatives.nGetFillType(nPath);
+  }
+
+  @Implementation(minSdk = O)
+  protected static void nSetFillType(long nPath, int ft) {
+    PathNatives.nSetFillType(nPath, ft);
+  }
+
+  @Implementation(minSdk = O)
+  protected static float[] nApproximate(long nPath, float error) {
+    return PathNatives.nApproximate(nPath, error);
+  }
+
+  @Override
+  public List<Point> getPoints() {
+    throw new UnsupportedOperationException("Legacy ShadowPath description APIs are not supported");
+  }
+
+  @Override
+  public void fillBounds(RectF bounds) {
+    throw new UnsupportedOperationException("Legacy ShadowPath description APIs are not supported");
+  }
+}
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativePathDashPathEffect.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativePathDashPathEffect.java
new file mode 100644
index 0000000..b72b0b2
--- /dev/null
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativePathDashPathEffect.java
@@ -0,0 +1,28 @@
+package org.robolectric.shadows;
+
+import static android.os.Build.VERSION_CODES.O;
+
+import android.graphics.PathDashPathEffect;
+import org.robolectric.annotation.Implementation;
+import org.robolectric.annotation.Implements;
+import org.robolectric.nativeruntime.DefaultNativeRuntimeLoader;
+import org.robolectric.nativeruntime.PathDashPathEffectNatives;
+import org.robolectric.shadows.ShadowNativePathDashPathEffect.Picker;
+
+/** Shadow for {@link PathDashPathEffect} that is backed by native code */
+@Implements(value = PathDashPathEffect.class, minSdk = O, shadowPicker = Picker.class)
+public class ShadowNativePathDashPathEffect {
+
+  @Implementation(minSdk = O)
+  protected static long nativeCreate(long nativePath, float advance, float phase, int nativeStyle) {
+    DefaultNativeRuntimeLoader.injectAndLoad();
+    return PathDashPathEffectNatives.nativeCreate(nativePath, advance, phase, nativeStyle);
+  }
+
+  /** Shadow picker for {@link PathDashPathEffect}. */
+  public static final class Picker extends GraphicsShadowPicker<Object> {
+    public Picker() {
+      super(null, ShadowNativePathDashPathEffect.class);
+    }
+  }
+}
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativePathEffect.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativePathEffect.java
new file mode 100644
index 0000000..440eb26
--- /dev/null
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativePathEffect.java
@@ -0,0 +1,26 @@
+package org.robolectric.shadows;
+
+import static android.os.Build.VERSION_CODES.O;
+
+import android.graphics.PathEffect;
+import org.robolectric.annotation.Implementation;
+import org.robolectric.annotation.Implements;
+import org.robolectric.nativeruntime.PathEffectNatives;
+import org.robolectric.shadows.ShadowNativePathEffect.Picker;
+
+/** Shadow for {@link PathEffect} that is backed by native code */
+@Implements(value = PathEffect.class, minSdk = O, shadowPicker = Picker.class)
+public class ShadowNativePathEffect {
+
+  @Implementation(minSdk = O)
+  protected static void nativeDestructor(long nativePatheffect) {
+    PathEffectNatives.nativeDestructor(nativePatheffect);
+  }
+
+  /** Shadow picker for {@link PathEffect}. */
+  public static final class Picker extends GraphicsShadowPicker<Object> {
+    public Picker() {
+      super(null, ShadowNativePathEffect.class);
+    }
+  }
+}
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativePathMeasure.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativePathMeasure.java
new file mode 100644
index 0000000..fd450e9
--- /dev/null
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativePathMeasure.java
@@ -0,0 +1,76 @@
+package org.robolectric.shadows;
+
+import static android.os.Build.VERSION_CODES.O;
+
+import android.graphics.PathMeasure;
+import org.robolectric.annotation.Implementation;
+import org.robolectric.annotation.Implements;
+import org.robolectric.nativeruntime.DefaultNativeRuntimeLoader;
+import org.robolectric.nativeruntime.PathMeasureNatives;
+import org.robolectric.shadows.ShadowNativePathMeasure.Picker;
+
+/** Shadow for {@link PathMeasure} that is backed by native code */
+@Implements(
+    value = PathMeasure.class,
+    minSdk = O,
+    shadowPicker = Picker.class,
+    isInAndroidSdk = false)
+public class ShadowNativePathMeasure {
+
+  @Implementation(minSdk = O)
+  protected static long native_create(long nativePath, boolean forceClosed) {
+    DefaultNativeRuntimeLoader.injectAndLoad();
+    return PathMeasureNatives.native_create(nativePath, forceClosed);
+  }
+
+  @Implementation(minSdk = O)
+  protected static void native_setPath(long nativeInstance, long nativePath, boolean forceClosed) {
+    PathMeasureNatives.native_setPath(nativeInstance, nativePath, forceClosed);
+  }
+
+  @Implementation(minSdk = O)
+  protected static float native_getLength(long nativeInstance) {
+    return PathMeasureNatives.native_getLength(nativeInstance);
+  }
+
+  @Implementation(minSdk = O)
+  protected static boolean native_getPosTan(
+      long nativeInstance, float distance, float[] pos, float[] tan) {
+    return PathMeasureNatives.native_getPosTan(nativeInstance, distance, pos, tan);
+  }
+
+  @Implementation(minSdk = O)
+  protected static boolean native_getMatrix(
+      long nativeInstance, float distance, long nativeMatrix, int flags) {
+    return PathMeasureNatives.native_getMatrix(nativeInstance, distance, nativeMatrix, flags);
+  }
+
+  @Implementation(minSdk = O)
+  protected static boolean native_getSegment(
+      long nativeInstance, float startD, float stopD, long nativePath, boolean startWithMoveTo) {
+    return PathMeasureNatives.native_getSegment(
+        nativeInstance, startD, stopD, nativePath, startWithMoveTo);
+  }
+
+  @Implementation(minSdk = O)
+  protected static boolean native_isClosed(long nativeInstance) {
+    return PathMeasureNatives.native_isClosed(nativeInstance);
+  }
+
+  @Implementation(minSdk = O)
+  protected static boolean native_nextContour(long nativeInstance) {
+    return PathMeasureNatives.native_nextContour(nativeInstance);
+  }
+
+  @Implementation(minSdk = O)
+  protected static void native_destroy(long nativeInstance) {
+    PathMeasureNatives.native_destroy(nativeInstance);
+  }
+
+  /** Shadow picker for {@link PathMeasure}. */
+  public static final class Picker extends GraphicsShadowPicker<Object> {
+    public Picker() {
+      super(ShadowPathMeasure.class, ShadowNativePathMeasure.class);
+    }
+  }
+}
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativePathParser.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativePathParser.java
new file mode 100644
index 0000000..cf83491
--- /dev/null
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativePathParser.java
@@ -0,0 +1,76 @@
+package org.robolectric.shadows;
+
+import static android.os.Build.VERSION_CODES.O;
+
+import android.util.PathParser;
+import org.robolectric.annotation.Implementation;
+import org.robolectric.annotation.Implements;
+import org.robolectric.nativeruntime.DefaultNativeRuntimeLoader;
+import org.robolectric.nativeruntime.PathParserNatives;
+import org.robolectric.shadows.ShadowNativePathParser.Picker;
+
+/** Shadow for {@link PathParser} that is backed by native code */
+@Implements(
+    value = PathParser.class,
+    minSdk = O,
+    shadowPicker = Picker.class,
+    isInAndroidSdk = false)
+public class ShadowNativePathParser {
+
+  static {
+    DefaultNativeRuntimeLoader.injectAndLoad();
+  }
+
+  @Implementation(minSdk = O)
+  protected static void nParseStringForPath(long pathPtr, String pathString, int stringLength) {
+    PathParserNatives.nParseStringForPath(pathPtr, pathString, stringLength);
+  }
+
+  @Implementation(minSdk = O)
+  protected static long nCreatePathDataFromString(String pathString, int stringLength) {
+    return PathParserNatives.nCreatePathDataFromString(pathString, stringLength);
+  }
+
+  @Implementation(minSdk = O)
+  protected static void nCreatePathFromPathData(long outPathPtr, long pathData) {
+    PathParserNatives.nCreatePathFromPathData(outPathPtr, pathData);
+  }
+
+  @Implementation(minSdk = O)
+  protected static long nCreateEmptyPathData() {
+    return PathParserNatives.nCreateEmptyPathData();
+  }
+
+  @Implementation(minSdk = O)
+  protected static long nCreatePathData(long nativePtr) {
+    return PathParserNatives.nCreatePathData(nativePtr);
+  }
+
+  @Implementation(minSdk = O)
+  protected static boolean nInterpolatePathData(
+      long outDataPtr, long fromDataPtr, long toDataPtr, float fraction) {
+    return PathParserNatives.nInterpolatePathData(outDataPtr, fromDataPtr, toDataPtr, fraction);
+  }
+
+  @Implementation(minSdk = O)
+  protected static void nFinalize(long nativePtr) {
+    PathParserNatives.nFinalize(nativePtr);
+  }
+
+  @Implementation(minSdk = O)
+  protected static boolean nCanMorph(long fromDataPtr, long toDataPtr) {
+    return PathParserNatives.nCanMorph(fromDataPtr, toDataPtr);
+  }
+
+  @Implementation(minSdk = O)
+  protected static void nSetPathData(long outDataPtr, long fromDataPtr) {
+    PathParserNatives.nSetPathData(outDataPtr, fromDataPtr);
+  }
+
+  /** Shadow picker for {@link PathParser}. */
+  public static final class Picker extends GraphicsShadowPicker<Object> {
+    public Picker() {
+      super(ShadowPathParser.class, ShadowNativePathParser.class);
+    }
+  }
+}
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativePicture.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativePicture.java
new file mode 100644
index 0000000..493076c
--- /dev/null
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativePicture.java
@@ -0,0 +1,72 @@
+package org.robolectric.shadows;
+
+import static android.os.Build.VERSION_CODES.O;
+
+import android.graphics.Picture;
+import java.io.InputStream;
+import java.io.OutputStream;
+import org.robolectric.annotation.Implementation;
+import org.robolectric.annotation.Implements;
+import org.robolectric.nativeruntime.DefaultNativeRuntimeLoader;
+import org.robolectric.nativeruntime.PictureNatives;
+import org.robolectric.shadows.ShadowNativePicture.Picker;
+
+/** Shadow for {@link Picture} that is backed by native code */
+@Implements(value = Picture.class, minSdk = O, shadowPicker = Picker.class, isInAndroidSdk = false)
+public class ShadowNativePicture {
+
+  @Implementation
+  protected static long nativeConstructor(long nativeSrcOr0) {
+    DefaultNativeRuntimeLoader.injectAndLoad();
+    return PictureNatives.nativeConstructor(nativeSrcOr0);
+  }
+
+  @Implementation
+  protected static long nativeCreateFromStream(InputStream stream, byte[] storage) {
+    DefaultNativeRuntimeLoader.injectAndLoad();
+    return PictureNatives.nativeCreateFromStream(stream, storage);
+  }
+
+  @Implementation
+  protected static int nativeGetWidth(long nativePicture) {
+    return PictureNatives.nativeGetWidth(nativePicture);
+  }
+
+  @Implementation
+  protected static int nativeGetHeight(long nativePicture) {
+    return PictureNatives.nativeGetHeight(nativePicture);
+  }
+
+  @Implementation
+  protected static long nativeBeginRecording(long nativeCanvas, int w, int h) {
+    return PictureNatives.nativeBeginRecording(nativeCanvas, w, h);
+  }
+
+  @Implementation
+  protected static void nativeEndRecording(long nativeCanvas) {
+    PictureNatives.nativeEndRecording(nativeCanvas);
+  }
+
+  @Implementation
+  protected static void nativeDraw(long nativeCanvas, long nativePicture) {
+    PictureNatives.nativeDraw(nativeCanvas, nativePicture);
+  }
+
+  @Implementation
+  protected static boolean nativeWriteToStream(
+      long nativePicture, OutputStream stream, byte[] storage) {
+    return PictureNatives.nativeWriteToStream(nativePicture, stream, storage);
+  }
+
+  @Implementation
+  protected static void nativeDestructor(long nativePicture) {
+    PictureNatives.nativeDestructor(nativePicture);
+  }
+
+  /** Shadow picker for {@link Picture}. */
+  public static final class Picker extends GraphicsShadowPicker<Object> {
+    public Picker() {
+      super(ShadowPicture.class, ShadowNativePicture.class);
+    }
+  }
+}
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativePorterDuffColorFilter.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativePorterDuffColorFilter.java
new file mode 100644
index 0000000..837ab51
--- /dev/null
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativePorterDuffColorFilter.java
@@ -0,0 +1,39 @@
+package org.robolectric.shadows;
+
+import static android.os.Build.VERSION_CODES.O;
+import static android.os.Build.VERSION_CODES.P;
+import static android.os.Build.VERSION_CODES.Q;
+
+import android.graphics.PorterDuffColorFilter;
+import org.robolectric.annotation.Implementation;
+import org.robolectric.annotation.Implements;
+import org.robolectric.nativeruntime.DefaultNativeRuntimeLoader;
+import org.robolectric.nativeruntime.PorterDuffColorFilterNatives;
+import org.robolectric.shadows.ShadowNativePorterDuffColorFilter.Picker;
+
+/** Shadow for {@link PorterDuffColorFilter} that is backed by native code */
+@Implements(
+    value = PorterDuffColorFilter.class,
+    minSdk = O,
+    shadowPicker = Picker.class,
+    isInAndroidSdk = false)
+public class ShadowNativePorterDuffColorFilter extends ShadowPorterDuffColorFilter {
+
+  @Implementation(minSdk = Q)
+  protected static long native_CreateBlendModeFilter(int srcColor, int blendmode) {
+    DefaultNativeRuntimeLoader.injectAndLoad();
+    return PorterDuffColorFilterNatives.native_CreateBlendModeFilter(srcColor, blendmode);
+  }
+
+  @Implementation(minSdk = O, maxSdk = P)
+  protected static long native_CreatePorterDuffFilter(int srcColor, int porterDuffMode) {
+    return native_CreateBlendModeFilter(srcColor, porterDuffMode);
+  }
+
+  /** Shadow picker for {@link PorterDuffColorFilter}. */
+  public static final class Picker extends GraphicsShadowPicker<ShadowPorterDuffColorFilter> {
+    public Picker() {
+      super(ShadowPorterDuffColorFilter.class, ShadowNativePorterDuffColorFilter.class);
+    }
+  }
+}
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativePropertyValuesHolder.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativePropertyValuesHolder.java
new file mode 100644
index 0000000..d9a8f7d
--- /dev/null
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativePropertyValuesHolder.java
@@ -0,0 +1,85 @@
+package org.robolectric.shadows;
+
+import static android.os.Build.VERSION_CODES.O;
+
+import android.animation.PropertyValuesHolder;
+import org.robolectric.annotation.Implementation;
+import org.robolectric.annotation.Implements;
+import org.robolectric.nativeruntime.PropertyValuesHolderNatives;
+import org.robolectric.shadows.ShadowNativePropertyValuesHolder.Picker;
+
+/** Shadow for {@link PropertyValuesHolder} that is backed by native code */
+@Implements(value = PropertyValuesHolder.class, minSdk = O, shadowPicker = Picker.class)
+public class ShadowNativePropertyValuesHolder {
+
+  @Implementation
+  protected static long nGetIntMethod(Class<?> targetClass, String methodName) {
+    return PropertyValuesHolderNatives.nGetIntMethod(targetClass, methodName);
+  }
+
+  @Implementation
+  protected static long nGetFloatMethod(Class<?> targetClass, String methodName) {
+    return PropertyValuesHolderNatives.nGetFloatMethod(targetClass, methodName);
+  }
+
+  @Implementation
+  protected static long nGetMultipleIntMethod(
+      Class<?> targetClass, String methodName, int numParams) {
+    return PropertyValuesHolderNatives.nGetMultipleIntMethod(targetClass, methodName, numParams);
+  }
+
+  @Implementation
+  protected static long nGetMultipleFloatMethod(
+      Class<?> targetClass, String methodName, int numParams) {
+    return PropertyValuesHolderNatives.nGetMultipleFloatMethod(targetClass, methodName, numParams);
+  }
+
+  @Implementation
+  protected static void nCallIntMethod(Object target, long methodID, int arg) {
+    PropertyValuesHolderNatives.nCallIntMethod(target, methodID, arg);
+  }
+
+  @Implementation
+  protected static void nCallFloatMethod(Object target, long methodID, float arg) {
+    PropertyValuesHolderNatives.nCallFloatMethod(target, methodID, arg);
+  }
+
+  @Implementation
+  protected static void nCallTwoIntMethod(Object target, long methodID, int arg1, int arg2) {
+    PropertyValuesHolderNatives.nCallTwoIntMethod(target, methodID, arg1, arg2);
+  }
+
+  @Implementation
+  protected static void nCallFourIntMethod(
+      Object target, long methodID, int arg1, int arg2, int arg3, int arg4) {
+    PropertyValuesHolderNatives.nCallFourIntMethod(target, methodID, arg1, arg2, arg3, arg4);
+  }
+
+  @Implementation
+  protected static void nCallMultipleIntMethod(Object target, long methodID, int[] args) {
+    PropertyValuesHolderNatives.nCallMultipleIntMethod(target, methodID, args);
+  }
+
+  @Implementation
+  protected static void nCallTwoFloatMethod(Object target, long methodID, float arg1, float arg2) {
+    PropertyValuesHolderNatives.nCallTwoFloatMethod(target, methodID, arg1, arg2);
+  }
+
+  @Implementation
+  protected static void nCallFourFloatMethod(
+      Object target, long methodID, float arg1, float arg2, float arg3, float arg4) {
+    PropertyValuesHolderNatives.nCallFourFloatMethod(target, methodID, arg1, arg2, arg3, arg4);
+  }
+
+  @Implementation
+  protected static void nCallMultipleFloatMethod(Object target, long methodID, float[] args) {
+    PropertyValuesHolderNatives.nCallMultipleFloatMethod(target, methodID, args);
+  }
+
+  /** Shadow picker for {@link PropertyValuesHolder}. */
+  public static final class Picker extends GraphicsShadowPicker<Object> {
+    public Picker() {
+      super(null, ShadowNativePropertyValuesHolder.class);
+    }
+  }
+}
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeRadialGradient.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeRadialGradient.java
new file mode 100644
index 0000000..07d2a72
--- /dev/null
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeRadialGradient.java
@@ -0,0 +1,83 @@
+package org.robolectric.shadows;
+
+import static android.os.Build.VERSION_CODES.O;
+import static android.os.Build.VERSION_CODES.P;
+import static android.os.Build.VERSION_CODES.Q;
+import static android.os.Build.VERSION_CODES.R;
+import static android.os.Build.VERSION_CODES.S;
+
+import android.graphics.RadialGradient;
+import androidx.annotation.ColorLong;
+import org.robolectric.annotation.Implementation;
+import org.robolectric.annotation.Implements;
+import org.robolectric.nativeruntime.DefaultNativeRuntimeLoader;
+import org.robolectric.nativeruntime.RadialGradientNatives;
+import org.robolectric.shadows.ShadowNativeRadialGradient.Picker;
+
+/** Shadow for {@link RadialGradient} that is backed by native code */
+@Implements(value = RadialGradient.class, minSdk = O, shadowPicker = Picker.class)
+public class ShadowNativeRadialGradient {
+
+  @Implementation(minSdk = S)
+  protected static long nativeCreate(
+      long matrix,
+      float startX,
+      float startY,
+      float startRadius,
+      float endX,
+      float endY,
+      float endRadius,
+      @ColorLong long[] colors,
+      float[] positions,
+      int tileMode,
+      long colorSpaceHandle) {
+    DefaultNativeRuntimeLoader.injectAndLoad();
+    return RadialGradientNatives.nativeCreate(
+        matrix,
+        startX,
+        startY,
+        startRadius,
+        endX,
+        endY,
+        endRadius,
+        colors,
+        positions,
+        tileMode,
+        colorSpaceHandle);
+  }
+
+  @Implementation(minSdk = Q, maxSdk = R)
+  protected static long nativeCreate(
+      long matrix,
+      float x,
+      float y,
+      float radius,
+      @ColorLong long[] colors,
+      float[] positions,
+      int tileMode,
+      long colorSpaceHandle) {
+    return nativeCreate(
+        matrix, x, y, 0, x, y, radius, colors, positions, tileMode, colorSpaceHandle);
+  }
+
+  @Implementation(minSdk = O, maxSdk = P)
+  protected static long nativeCreate1(
+      long matrix, float x, float y, float radius, int[] colors, float[] positions, int tileMode) {
+    DefaultNativeRuntimeLoader.injectAndLoad();
+    return RadialGradientNatives.nativeCreate1(matrix, x, y, radius, colors, positions, tileMode);
+  }
+
+  @Implementation(minSdk = O, maxSdk = P)
+  protected static long nativeCreate2(
+      long matrix, float x, float y, float radius, int color0, int color1, int tileMode) {
+    DefaultNativeRuntimeLoader.injectAndLoad();
+    return RadialGradientNatives.nativeCreate2(matrix, x, y, radius, color0, color1, tileMode);
+  }
+
+  /** Shadow picker for {@link RadialGradient}. */
+  public static final class Picker extends GraphicsShadowPicker<Object> {
+    public Picker() {
+      super(null, ShadowNativeRadialGradient.class);
+    }
+  }
+}
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeRecordingCanvas.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeRecordingCanvas.java
new file mode 100644
index 0000000..537419c
--- /dev/null
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeRecordingCanvas.java
@@ -0,0 +1,112 @@
+package org.robolectric.shadows;
+
+import static android.os.Build.VERSION_CODES.Q;
+import static android.os.Build.VERSION_CODES.S;
+
+import android.graphics.RecordingCanvas;
+import org.robolectric.annotation.Implementation;
+import org.robolectric.annotation.Implements;
+import org.robolectric.nativeruntime.DefaultNativeRuntimeLoader;
+import org.robolectric.nativeruntime.RecordingCanvasNatives;
+import org.robolectric.shadows.ShadowNativeRecordingCanvas.Picker;
+
+/** Shadow for {@link RecordingCanvas} that is backed by native code */
+@Implements(value = RecordingCanvas.class, minSdk = Q, shadowPicker = Picker.class)
+public class ShadowNativeRecordingCanvas extends ShadowNativeBaseRecordingCanvas {
+
+  @Implementation
+  protected static long nCreateDisplayListCanvas(long node, int width, int height) {
+    DefaultNativeRuntimeLoader.injectAndLoad();
+    return RecordingCanvasNatives.nCreateDisplayListCanvas(node, width, height);
+  }
+
+  @Implementation
+  protected static void nResetDisplayListCanvas(long canvas, long node, int width, int height) {
+    RecordingCanvasNatives.nResetDisplayListCanvas(canvas, node, width, height);
+  }
+
+  @Implementation
+  protected static int nGetMaximumTextureWidth() {
+    return RecordingCanvasNatives.nGetMaximumTextureWidth();
+  }
+
+  @Implementation
+  protected static int nGetMaximumTextureHeight() {
+    return RecordingCanvasNatives.nGetMaximumTextureHeight();
+  }
+
+  @Implementation(minSdk = S)
+  protected static void nEnableZ(long renderer, boolean enableZ) {
+    RecordingCanvasNatives.nEnableZ(renderer, enableZ);
+  }
+
+  @Implementation(minSdk = S)
+  protected static void nFinishRecording(long renderer, long renderNode) {
+    RecordingCanvasNatives.nFinishRecording(renderer, renderNode);
+  }
+
+  @Implementation
+  protected static void nDrawRenderNode(long renderer, long renderNode) {
+    RecordingCanvasNatives.nDrawRenderNode(renderer, renderNode);
+  }
+
+  @Implementation
+  protected static void nDrawTextureLayer(long renderer, long layer) {
+    RecordingCanvasNatives.nDrawTextureLayer(renderer, layer);
+  }
+
+  @Implementation
+  protected static void nDrawCircle(
+      long renderer, long propCx, long propCy, long propRadius, long propPaint) {
+    RecordingCanvasNatives.nDrawCircle(renderer, propCx, propCy, propRadius, propPaint);
+  }
+
+  @Implementation(minSdk = S)
+  protected static void nDrawRipple(
+      long renderer,
+      long propCx,
+      long propCy,
+      long propRadius,
+      long propPaint,
+      long propProgress,
+      long turbulencePhase,
+      int color,
+      long runtimeEffect) {
+    RecordingCanvasNatives.nDrawRipple(
+        renderer,
+        propCx,
+        propCy,
+        propRadius,
+        propPaint,
+        propProgress,
+        turbulencePhase,
+        color,
+        runtimeEffect);
+  }
+
+  @Implementation
+  protected static void nDrawRoundRect(
+      long renderer,
+      long propLeft,
+      long propTop,
+      long propRight,
+      long propBottom,
+      long propRx,
+      long propRy,
+      long propPaint) {
+    RecordingCanvasNatives.nDrawRoundRect(
+        renderer, propLeft, propTop, propRight, propBottom, propRx, propRy, propPaint);
+  }
+
+  @Implementation
+  protected static void nDrawWebViewFunctor(long canvas, int functor) {
+    RecordingCanvasNatives.nDrawWebViewFunctor(canvas, functor);
+  }
+
+  /** Shadow picker for {@link RecordingCanvas}. */
+  public static final class Picker extends GraphicsShadowPicker<Object> {
+    public Picker() {
+      super(ShadowRecordingCanvas.class, ShadowNativeRecordingCanvas.class);
+    }
+  }
+}
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeRegion.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeRegion.java
new file mode 100644
index 0000000..6c855c4
--- /dev/null
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeRegion.java
@@ -0,0 +1,184 @@
+package org.robolectric.shadows;
+
+import static android.os.Build.VERSION_CODES.O;
+import static org.robolectric.shadow.api.Shadow.invokeConstructor;
+import static org.robolectric.util.reflector.Reflector.reflector;
+
+import android.graphics.Rect;
+import android.graphics.Region;
+import android.os.Parcel;
+import com.google.errorprone.annotations.DoNotCall;
+import org.robolectric.annotation.Implementation;
+import org.robolectric.annotation.Implements;
+import org.robolectric.annotation.RealObject;
+import org.robolectric.nativeruntime.DefaultNativeRuntimeLoader;
+import org.robolectric.nativeruntime.RegionNatives;
+import org.robolectric.shadows.ShadowNativeRegion.Picker;
+import org.robolectric.util.ReflectionHelpers.ClassParameter;
+import org.robolectric.util.reflector.Accessor;
+import org.robolectric.util.reflector.ForType;
+
+/** Shadow for {@link Region} that is backed by native code */
+@Implements(value = Region.class, minSdk = O, shadowPicker = Picker.class, isInAndroidSdk = false)
+public class ShadowNativeRegion {
+
+  RegionNatives regionNatives = new RegionNatives();
+  @RealObject Region realRegion;
+
+  @Implementation(minSdk = O)
+  protected void __constructor__(long ni) {
+    invokeConstructor(Region.class, realRegion, ClassParameter.from(long.class, ni));
+    regionNatives.mNativeRegion = ni;
+  }
+
+  @Implementation(minSdk = O)
+  protected void __constructor__(int left, int top, int right, int bottom) {
+    invokeConstructor(
+        Region.class,
+        realRegion,
+        ClassParameter.from(int.class, left),
+        ClassParameter.from(int.class, top),
+        ClassParameter.from(int.class, right),
+        ClassParameter.from(int.class, bottom));
+    regionNatives.mNativeRegion = reflector(RegionReflector.class, realRegion).getNativeRegion();
+  }
+
+  @Implementation(minSdk = O)
+  protected void __constructor__(Rect rect) {
+    invokeConstructor(Region.class, realRegion, ClassParameter.from(Rect.class, rect));
+    regionNatives.mNativeRegion = reflector(RegionReflector.class, realRegion).getNativeRegion();
+  }
+
+  @Implementation(minSdk = O)
+  protected static boolean nativeEquals(long nativeR1, long nativeR2) {
+    return RegionNatives.nativeEquals(nativeR1, nativeR2);
+  }
+
+  @Implementation(minSdk = O)
+  protected static long nativeConstructor() {
+    DefaultNativeRuntimeLoader.injectAndLoad();
+    return RegionNatives.nativeConstructor();
+  }
+
+  @Implementation(minSdk = O)
+  protected static void nativeDestructor(long nativeRegion) {
+    RegionNatives.nativeDestructor(nativeRegion);
+  }
+
+  @Implementation(minSdk = O)
+  protected static void nativeSetRegion(long nativeDst, long nativeSrc) {
+    RegionNatives.nativeSetRegion(nativeDst, nativeSrc);
+  }
+
+  @Implementation(minSdk = O)
+  protected static boolean nativeSetRect(long nativeDst, int left, int top, int right, int bottom) {
+    return RegionNatives.nativeSetRect(nativeDst, left, top, right, bottom);
+  }
+
+  @Implementation(minSdk = O)
+  protected static boolean nativeSetPath(long nativeDst, long nativePath, long nativeClip) {
+    return RegionNatives.nativeSetPath(nativeDst, nativePath, nativeClip);
+  }
+
+  @Implementation(minSdk = O)
+  protected static boolean nativeGetBounds(long nativeRegion, Rect rect) {
+    return RegionNatives.nativeGetBounds(nativeRegion, rect);
+  }
+
+  @Implementation(minSdk = O)
+  protected static boolean nativeGetBoundaryPath(long nativeRegion, long nativePath) {
+    return RegionNatives.nativeGetBoundaryPath(nativeRegion, nativePath);
+  }
+
+  @Implementation(minSdk = O)
+  protected static boolean nativeOp(
+      long nativeDst, int left, int top, int right, int bottom, int op) {
+    return RegionNatives.nativeOp(nativeDst, left, top, right, bottom, op);
+  }
+
+  @Implementation(minSdk = O)
+  protected static boolean nativeOp(long nativeDst, Rect rect, long nativeRegion, int op) {
+    return RegionNatives.nativeOp(nativeDst, rect, nativeRegion, op);
+  }
+
+  @Implementation(minSdk = O)
+  protected static boolean nativeOp(
+      long nativeDst, long nativeRegion1, long nativeRegion2, int op) {
+    return RegionNatives.nativeOp(nativeDst, nativeRegion1, nativeRegion2, op);
+  }
+
+  @DoNotCall("Always throws java.lang.UnsupportedOperationException")
+  @Implementation(minSdk = O)
+  protected static long nativeCreateFromParcel(Parcel p) {
+    throw new UnsupportedOperationException();
+  }
+
+  @DoNotCall("Always throws java.lang.UnsupportedOperationException")
+  @Implementation(minSdk = O)
+  protected static boolean nativeWriteToParcel(long nativeRegion, Parcel p) {
+    throw new UnsupportedOperationException();
+  }
+
+  @Implementation(minSdk = O)
+  protected static String nativeToString(long nativeRegion) {
+    return RegionNatives.nativeToString(nativeRegion);
+  }
+
+  @Implementation(minSdk = O)
+  protected boolean isEmpty() {
+    return regionNatives.isEmpty();
+  }
+
+  @Implementation(minSdk = O)
+  protected boolean isRect() {
+    return regionNatives.isRect();
+  }
+
+  @Implementation(minSdk = O)
+  protected boolean isComplex() {
+    return regionNatives.isComplex();
+  }
+
+  @Implementation(minSdk = O)
+  protected boolean contains(int x, int y) {
+    return regionNatives.contains(x, y);
+  }
+
+  @Implementation(minSdk = O)
+  protected boolean quickContains(int left, int top, int right, int bottom) {
+    return regionNatives.quickContains(left, top, right, bottom);
+  }
+
+  @Implementation(minSdk = O)
+  protected boolean quickReject(int left, int top, int right, int bottom) {
+    return regionNatives.quickReject(left, top, right, bottom);
+  }
+
+  @Implementation(minSdk = O)
+  protected boolean quickReject(Region rgn) {
+    return regionNatives.quickReject(rgn);
+  }
+
+  @Implementation(minSdk = O)
+  protected void translate(int dx, int dy, Region dst) {
+    regionNatives.translate(dx, dy, dst);
+  }
+
+  @Implementation(minSdk = O)
+  protected void scale(float scale, Region dst) {
+    regionNatives.scale(scale, dst);
+  }
+
+  @ForType(Region.class)
+  interface RegionReflector {
+    @Accessor("mNativeRegion")
+    long getNativeRegion();
+  }
+
+  /** Shadow picker for {@link Region}. */
+  public static final class Picker extends GraphicsShadowPicker<Object> {
+    public Picker() {
+      super(ShadowRegion.class, ShadowNativeRegion.class);
+    }
+  }
+}
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeRegionIterator.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeRegionIterator.java
new file mode 100644
index 0000000..b47bd42
--- /dev/null
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeRegionIterator.java
@@ -0,0 +1,39 @@
+package org.robolectric.shadows;
+
+import static android.os.Build.VERSION_CODES.O;
+
+import android.graphics.Rect;
+import android.graphics.RegionIterator;
+import org.robolectric.annotation.Implementation;
+import org.robolectric.annotation.Implements;
+import org.robolectric.nativeruntime.DefaultNativeRuntimeLoader;
+import org.robolectric.nativeruntime.RegionIteratorNatives;
+import org.robolectric.shadows.ShadowNativeRegionIterator.Picker;
+
+/** Shadow for {@link RegionIterator} that is backed by native code */
+@Implements(value = RegionIterator.class, minSdk = O, shadowPicker = Picker.class)
+public class ShadowNativeRegionIterator {
+
+  @Implementation(minSdk = O)
+  protected static long nativeConstructor(long nativeRegion) {
+    DefaultNativeRuntimeLoader.injectAndLoad();
+    return RegionIteratorNatives.nativeConstructor(nativeRegion);
+  }
+
+  @Implementation(minSdk = O)
+  protected static void nativeDestructor(long nativeIter) {
+    RegionIteratorNatives.nativeDestructor(nativeIter);
+  }
+
+  @Implementation(minSdk = O)
+  protected static boolean nativeNext(long nativeIter, Rect r) {
+    return RegionIteratorNatives.nativeNext(nativeIter, r);
+  }
+
+  /** Shadow picker for {@link RegionIterator}. */
+  public static final class Picker extends GraphicsShadowPicker<Object> {
+    public Picker() {
+      super(null, ShadowNativeRegionIterator.class);
+    }
+  }
+}
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeRenderEffect.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeRenderEffect.java
new file mode 100644
index 0000000..0535803
--- /dev/null
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeRenderEffect.java
@@ -0,0 +1,77 @@
+package org.robolectric.shadows;
+
+import static android.os.Build.VERSION_CODES.O;
+import static android.os.Build.VERSION_CODES.S;
+
+import android.graphics.RenderEffect;
+import org.robolectric.annotation.Implementation;
+import org.robolectric.annotation.Implements;
+import org.robolectric.nativeruntime.DefaultNativeRuntimeLoader;
+import org.robolectric.nativeruntime.RenderEffectNatives;
+import org.robolectric.shadows.ShadowNativeRenderEffect.Picker;
+
+/** Shadow for {@link RenderEffect} that is backed by native code */
+@Implements(value = RenderEffect.class, minSdk = O, shadowPicker = Picker.class)
+public class ShadowNativeRenderEffect {
+  static {
+    DefaultNativeRuntimeLoader.injectAndLoad();
+  }
+
+  @Implementation(minSdk = S)
+  protected static long nativeCreateOffsetEffect(float offsetX, float offsetY, long nativeInput) {
+    return RenderEffectNatives.nativeCreateOffsetEffect(offsetX, offsetY, nativeInput);
+  }
+
+  @Implementation(minSdk = S)
+  protected static long nativeCreateBlurEffect(
+      float radiusX, float radiusY, long nativeInput, int edgeTreatment) {
+    return RenderEffectNatives.nativeCreateBlurEffect(radiusX, radiusY, nativeInput, edgeTreatment);
+  }
+
+  @Implementation(minSdk = S)
+  protected static long nativeCreateBitmapEffect(
+      long bitmapHandle,
+      float srcLeft,
+      float srcTop,
+      float srcRight,
+      float srcBottom,
+      float dstLeft,
+      float dstTop,
+      float dstRight,
+      float dstBottom) {
+    return RenderEffectNatives.nativeCreateBitmapEffect(
+        bitmapHandle, srcLeft, srcTop, srcRight, srcBottom, dstLeft, dstTop, dstRight, dstBottom);
+  }
+
+  @Implementation(minSdk = S)
+  protected static long nativeCreateColorFilterEffect(long colorFilter, long nativeInput) {
+    return RenderEffectNatives.nativeCreateColorFilterEffect(colorFilter, nativeInput);
+  }
+
+  @Implementation(minSdk = S)
+  protected static long nativeCreateBlendModeEffect(long dst, long src, int blendmode) {
+    return RenderEffectNatives.nativeCreateBlendModeEffect(dst, src, blendmode);
+  }
+
+  @Implementation(minSdk = S)
+  protected static long nativeCreateChainEffect(long outer, long inner) {
+    return RenderEffectNatives.nativeCreateChainEffect(outer, inner);
+  }
+
+  @Implementation(minSdk = S)
+  protected static long nativeCreateShaderEffect(long shader) {
+    return RenderEffectNatives.nativeCreateShaderEffect(shader);
+  }
+
+  @Implementation(minSdk = S)
+  protected static long nativeGetFinalizer() {
+    return RenderEffectNatives.nativeGetFinalizer();
+  }
+
+  /** Shadow picker for {@link RenderEffect}. */
+  public static final class Picker extends GraphicsShadowPicker<Object> {
+    public Picker() {
+      super(null, ShadowNativeRenderEffect.class);
+    }
+  }
+}
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeRenderNode.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeRenderNode.java
new file mode 100644
index 0000000..4b11355
--- /dev/null
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeRenderNode.java
@@ -0,0 +1,468 @@
+package org.robolectric.shadows;
+
+import static android.os.Build.VERSION_CODES.Q;
+import static android.os.Build.VERSION_CODES.R;
+import static android.os.Build.VERSION_CODES.S;
+import static android.os.Build.VERSION_CODES.S_V2;
+
+import android.graphics.RenderNode;
+import android.graphics.RenderNode.PositionUpdateListener;
+import org.robolectric.annotation.Implementation;
+import org.robolectric.annotation.Implements;
+import org.robolectric.nativeruntime.DefaultNativeRuntimeLoader;
+import org.robolectric.nativeruntime.RenderNodeNatives;
+import org.robolectric.shadows.ShadowNativeRenderNode.Picker;
+
+/** Shadow for {@link RenderNode} that is backed by native code */
+@Implements(value = RenderNode.class, minSdk = Q, shadowPicker = Picker.class)
+public class ShadowNativeRenderNode {
+  @Implementation
+  protected static long nCreate(String name) {
+    DefaultNativeRuntimeLoader.injectAndLoad();
+    return RenderNodeNatives.nCreate(name);
+  }
+
+  @Implementation
+  protected static long nGetNativeFinalizer() {
+    return RenderNodeNatives.nGetNativeFinalizer();
+  }
+
+  @Implementation
+  protected static void nOutput(long renderNode) {
+    RenderNodeNatives.nOutput(renderNode);
+  }
+
+  @Implementation(minSdk = R)
+  protected static int nGetUsageSize(long renderNode) {
+    return RenderNodeNatives.nGetUsageSize(renderNode);
+  }
+
+  @Implementation(minSdk = R)
+  protected static int nGetAllocatedSize(long renderNode) {
+    return RenderNodeNatives.nGetAllocatedSize(renderNode);
+  }
+
+  @Implementation(maxSdk = S_V2)
+  protected static void nRequestPositionUpdates(long renderNode, PositionUpdateListener callback) {
+    RenderNodeNatives.nRequestPositionUpdates(renderNode, callback);
+  }
+
+  @Implementation
+  protected static void nAddAnimator(long renderNode, long animatorPtr) {
+    RenderNodeNatives.nAddAnimator(renderNode, animatorPtr);
+  }
+
+  @Implementation
+  protected static void nEndAllAnimators(long renderNode) {
+    RenderNodeNatives.nEndAllAnimators(renderNode);
+  }
+
+  @Implementation(minSdk = S)
+  protected static void nDiscardDisplayList(long renderNode) {
+    RenderNodeNatives.nDiscardDisplayList(renderNode);
+  }
+
+  @Implementation
+  protected static boolean nIsValid(long renderNode) {
+    return RenderNodeNatives.nIsValid(renderNode);
+  }
+
+  @Implementation
+  protected static void nGetTransformMatrix(long renderNode, long nativeMatrix) {
+    RenderNodeNatives.nGetTransformMatrix(renderNode, nativeMatrix);
+  }
+
+  @Implementation
+  protected static void nGetInverseTransformMatrix(long renderNode, long nativeMatrix) {
+    RenderNodeNatives.nGetInverseTransformMatrix(renderNode, nativeMatrix);
+  }
+
+  @Implementation
+  protected static boolean nHasIdentityMatrix(long renderNode) {
+    return RenderNodeNatives.nHasIdentityMatrix(renderNode);
+  }
+
+  @Implementation
+  protected static boolean nOffsetTopAndBottom(long renderNode, int offset) {
+    return RenderNodeNatives.nOffsetTopAndBottom(renderNode, offset);
+  }
+
+  @Implementation
+  protected static boolean nOffsetLeftAndRight(long renderNode, int offset) {
+    return RenderNodeNatives.nOffsetLeftAndRight(renderNode, offset);
+  }
+
+  @Implementation
+  protected static boolean nSetLeftTopRightBottom(
+      long renderNode, int left, int top, int right, int bottom) {
+    return RenderNodeNatives.nSetLeftTopRightBottom(renderNode, left, top, right, bottom);
+  }
+
+  @Implementation
+  protected static boolean nSetLeft(long renderNode, int left) {
+    return RenderNodeNatives.nSetLeft(renderNode, left);
+  }
+
+  @Implementation
+  protected static boolean nSetTop(long renderNode, int top) {
+    return RenderNodeNatives.nSetTop(renderNode, top);
+  }
+
+  @Implementation
+  protected static boolean nSetRight(long renderNode, int right) {
+    return RenderNodeNatives.nSetRight(renderNode, right);
+  }
+
+  @Implementation
+  protected static boolean nSetBottom(long renderNode, int bottom) {
+    return RenderNodeNatives.nSetBottom(renderNode, bottom);
+  }
+
+  @Implementation
+  protected static int nGetLeft(long renderNode) {
+    return RenderNodeNatives.nGetLeft(renderNode);
+  }
+
+  @Implementation
+  protected static int nGetTop(long renderNode) {
+    return RenderNodeNatives.nGetTop(renderNode);
+  }
+
+  @Implementation
+  protected static int nGetRight(long renderNode) {
+    return RenderNodeNatives.nGetRight(renderNode);
+  }
+
+  @Implementation
+  protected static int nGetBottom(long renderNode) {
+    return RenderNodeNatives.nGetBottom(renderNode);
+  }
+
+  @Implementation
+  protected static boolean nSetCameraDistance(long renderNode, float distance) {
+    return RenderNodeNatives.nSetCameraDistance(renderNode, distance);
+  }
+
+  @Implementation
+  protected static boolean nSetPivotY(long renderNode, float pivotY) {
+    return RenderNodeNatives.nSetPivotY(renderNode, pivotY);
+  }
+
+  @Implementation
+  protected static boolean nSetPivotX(long renderNode, float pivotX) {
+    return RenderNodeNatives.nSetPivotX(renderNode, pivotX);
+  }
+
+  @Implementation
+  protected static boolean nResetPivot(long renderNode) {
+    return RenderNodeNatives.nResetPivot(renderNode);
+  }
+
+  @Implementation
+  protected static boolean nSetLayerType(long renderNode, int layerType) {
+    return RenderNodeNatives.nSetLayerType(renderNode, layerType);
+  }
+
+  @Implementation
+  protected static int nGetLayerType(long renderNode) {
+    return RenderNodeNatives.nGetLayerType(renderNode);
+  }
+
+  @Implementation
+  protected static boolean nSetLayerPaint(long renderNode, long paint) {
+    return RenderNodeNatives.nSetLayerPaint(renderNode, paint);
+  }
+
+  @Implementation
+  protected static boolean nSetClipToBounds(long renderNode, boolean clipToBounds) {
+    return RenderNodeNatives.nSetClipToBounds(renderNode, clipToBounds);
+  }
+
+  @Implementation
+  protected static boolean nGetClipToBounds(long renderNode) {
+    return RenderNodeNatives.nGetClipToBounds(renderNode);
+  }
+
+  @Implementation
+  protected static boolean nSetClipBounds(
+      long renderNode, int left, int top, int right, int bottom) {
+    return RenderNodeNatives.nSetClipBounds(renderNode, left, top, right, bottom);
+  }
+
+  @Implementation
+  protected static boolean nSetClipBoundsEmpty(long renderNode) {
+    return RenderNodeNatives.nSetClipBoundsEmpty(renderNode);
+  }
+
+  @Implementation
+  protected static boolean nSetProjectBackwards(long renderNode, boolean shouldProject) {
+    return RenderNodeNatives.nSetProjectBackwards(renderNode, shouldProject);
+  }
+
+  @Implementation
+  protected static boolean nSetProjectionReceiver(long renderNode, boolean shouldReceive) {
+    return RenderNodeNatives.nSetProjectionReceiver(renderNode, shouldReceive);
+  }
+
+  @Implementation
+  protected static boolean nSetOutlineRoundRect(
+      long renderNode, int left, int top, int right, int bottom, float radius, float alpha) {
+    return RenderNodeNatives.nSetOutlineRoundRect(
+        renderNode, left, top, right, bottom, radius, alpha);
+  }
+
+  @Implementation(minSdk = R)
+  protected static boolean nSetOutlinePath(long renderNode, long nativePath, float alpha) {
+    return RenderNodeNatives.nSetOutlinePath(renderNode, nativePath, alpha);
+  }
+
+  @Implementation
+  protected static boolean nSetOutlineEmpty(long renderNode) {
+    return RenderNodeNatives.nSetOutlineEmpty(renderNode);
+  }
+
+  @Implementation
+  protected static boolean nSetOutlineNone(long renderNode) {
+    return RenderNodeNatives.nSetOutlineNone(renderNode);
+  }
+
+  @Implementation(minSdk = S)
+  protected static boolean nClearStretch(long renderNode) {
+    return RenderNodeNatives.nClearStretch(renderNode);
+  }
+
+  @Implementation(minSdk = S)
+  protected static boolean nStretch(
+      long renderNode, float vecX, float vecY, float maxStretchX, float maxStretchY) {
+    return RenderNodeNatives.nStretch(renderNode, vecX, vecY, maxStretchX, maxStretchY);
+  }
+
+  @Implementation
+  protected static boolean nHasShadow(long renderNode) {
+    return RenderNodeNatives.nHasShadow(renderNode);
+  }
+
+  @Implementation
+  protected static boolean nSetSpotShadowColor(long renderNode, int color) {
+    return RenderNodeNatives.nSetSpotShadowColor(renderNode, color);
+  }
+
+  @Implementation
+  protected static boolean nSetAmbientShadowColor(long renderNode, int color) {
+    return RenderNodeNatives.nSetAmbientShadowColor(renderNode, color);
+  }
+
+  @Implementation
+  protected static int nGetSpotShadowColor(long renderNode) {
+    return RenderNodeNatives.nGetSpotShadowColor(renderNode);
+  }
+
+  @Implementation
+  protected static int nGetAmbientShadowColor(long renderNode) {
+    return RenderNodeNatives.nGetAmbientShadowColor(renderNode);
+  }
+
+  @Implementation
+  protected static boolean nSetClipToOutline(long renderNode, boolean clipToOutline) {
+    return RenderNodeNatives.nSetClipToOutline(renderNode, clipToOutline);
+  }
+
+  @Implementation
+  protected static boolean nSetRevealClip(
+      long renderNode, boolean shouldClip, float x, float y, float radius) {
+    return RenderNodeNatives.nSetRevealClip(renderNode, shouldClip, x, y, radius);
+  }
+
+  @Implementation
+  protected static boolean nSetAlpha(long renderNode, float alpha) {
+    return RenderNodeNatives.nSetAlpha(renderNode, alpha);
+  }
+
+  @Implementation(minSdk = S)
+  protected static boolean nSetRenderEffect(long renderNode, long renderEffect) {
+    return RenderNodeNatives.nSetRenderEffect(renderNode, renderEffect);
+  }
+
+  @Implementation
+  protected static boolean nSetHasOverlappingRendering(
+      long renderNode, boolean hasOverlappingRendering) {
+    return RenderNodeNatives.nSetHasOverlappingRendering(renderNode, hasOverlappingRendering);
+  }
+
+  @Implementation
+  protected static void nSetUsageHint(long renderNode, int usageHint) {
+    RenderNodeNatives.nSetUsageHint(renderNode, usageHint);
+  }
+
+  @Implementation
+  protected static boolean nSetElevation(long renderNode, float lift) {
+    return RenderNodeNatives.nSetElevation(renderNode, lift);
+  }
+
+  @Implementation
+  protected static boolean nSetTranslationX(long renderNode, float translationX) {
+    return RenderNodeNatives.nSetTranslationX(renderNode, translationX);
+  }
+
+  @Implementation
+  protected static boolean nSetTranslationY(long renderNode, float translationY) {
+    return RenderNodeNatives.nSetTranslationY(renderNode, translationY);
+  }
+
+  @Implementation
+  protected static boolean nSetTranslationZ(long renderNode, float translationZ) {
+    return RenderNodeNatives.nSetTranslationZ(renderNode, translationZ);
+  }
+
+  @Implementation
+  protected static boolean nSetRotation(long renderNode, float rotation) {
+    return RenderNodeNatives.nSetRotation(renderNode, rotation);
+  }
+
+  @Implementation
+  protected static boolean nSetRotationX(long renderNode, float rotationX) {
+    return RenderNodeNatives.nSetRotationX(renderNode, rotationX);
+  }
+
+  @Implementation
+  protected static boolean nSetRotationY(long renderNode, float rotationY) {
+    return RenderNodeNatives.nSetRotationY(renderNode, rotationY);
+  }
+
+  @Implementation
+  protected static boolean nSetScaleX(long renderNode, float scaleX) {
+    return RenderNodeNatives.nSetScaleX(renderNode, scaleX);
+  }
+
+  @Implementation
+  protected static boolean nSetScaleY(long renderNode, float scaleY) {
+    return RenderNodeNatives.nSetScaleY(renderNode, scaleY);
+  }
+
+  @Implementation
+  protected static boolean nSetStaticMatrix(long renderNode, long nativeMatrix) {
+    return RenderNodeNatives.nSetStaticMatrix(renderNode, nativeMatrix);
+  }
+
+  @Implementation
+  protected static boolean nSetAnimationMatrix(long renderNode, long animationMatrix) {
+    return RenderNodeNatives.nSetAnimationMatrix(renderNode, animationMatrix);
+  }
+
+  @Implementation
+  protected static boolean nHasOverlappingRendering(long renderNode) {
+    return RenderNodeNatives.nHasOverlappingRendering(renderNode);
+  }
+
+  @Implementation
+  protected static boolean nGetAnimationMatrix(long renderNode, long animationMatrix) {
+    return RenderNodeNatives.nGetAnimationMatrix(renderNode, animationMatrix);
+  }
+
+  @Implementation
+  protected static boolean nGetClipToOutline(long renderNode) {
+    return RenderNodeNatives.nGetClipToOutline(renderNode);
+  }
+
+  @Implementation
+  protected static float nGetAlpha(long renderNode) {
+    return RenderNodeNatives.nGetAlpha(renderNode);
+  }
+
+  @Implementation
+  protected static float nGetCameraDistance(long renderNode) {
+    return RenderNodeNatives.nGetCameraDistance(renderNode);
+  }
+
+  @Implementation
+  protected static float nGetScaleX(long renderNode) {
+    return RenderNodeNatives.nGetScaleX(renderNode);
+  }
+
+  @Implementation
+  protected static float nGetScaleY(long renderNode) {
+    return RenderNodeNatives.nGetScaleY(renderNode);
+  }
+
+  @Implementation
+  protected static float nGetElevation(long renderNode) {
+    return RenderNodeNatives.nGetElevation(renderNode);
+  }
+
+  @Implementation
+  protected static float nGetTranslationX(long renderNode) {
+    return RenderNodeNatives.nGetTranslationX(renderNode);
+  }
+
+  @Implementation
+  protected static float nGetTranslationY(long renderNode) {
+    return RenderNodeNatives.nGetTranslationY(renderNode);
+  }
+
+  @Implementation
+  protected static float nGetTranslationZ(long renderNode) {
+    return RenderNodeNatives.nGetTranslationZ(renderNode);
+  }
+
+  @Implementation
+  protected static float nGetRotation(long renderNode) {
+    return RenderNodeNatives.nGetRotation(renderNode);
+  }
+
+  @Implementation
+  protected static float nGetRotationX(long renderNode) {
+    return RenderNodeNatives.nGetRotationX(renderNode);
+  }
+
+  @Implementation
+  protected static float nGetRotationY(long renderNode) {
+    return RenderNodeNatives.nGetRotationY(renderNode);
+  }
+
+  @Implementation
+  protected static boolean nIsPivotExplicitlySet(long renderNode) {
+    return RenderNodeNatives.nIsPivotExplicitlySet(renderNode);
+  }
+
+  @Implementation
+  protected static float nGetPivotX(long renderNode) {
+    return RenderNodeNatives.nGetPivotX(renderNode);
+  }
+
+  @Implementation
+  protected static float nGetPivotY(long renderNode) {
+    return RenderNodeNatives.nGetPivotY(renderNode);
+  }
+
+  @Implementation
+  protected static int nGetWidth(long renderNode) {
+    return RenderNodeNatives.nGetWidth(renderNode);
+  }
+
+  @Implementation
+  protected static int nGetHeight(long renderNode) {
+    return RenderNodeNatives.nGetHeight(renderNode);
+  }
+
+  @Implementation
+  protected static boolean nSetAllowForceDark(long renderNode, boolean allowForceDark) {
+    return RenderNodeNatives.nSetAllowForceDark(renderNode, allowForceDark);
+  }
+
+  @Implementation
+  protected static boolean nGetAllowForceDark(long renderNode) {
+    return RenderNodeNatives.nGetAllowForceDark(renderNode);
+  }
+
+  @Implementation
+  protected static long nGetUniqueId(long renderNode) {
+    return RenderNodeNatives.nGetUniqueId(renderNode);
+  }
+
+  /** Shadow picker for {@link RenderNode}. */
+  public static final class Picker extends GraphicsShadowPicker<Object> {
+    public Picker() {
+      super(ShadowRenderNodeQ.class, ShadowNativeRenderNode.class);
+    }
+  }
+}
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeRenderNodeAnimator.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeRenderNodeAnimator.java
new file mode 100644
index 0000000..5b3c32a
--- /dev/null
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeRenderNodeAnimator.java
@@ -0,0 +1,96 @@
+package org.robolectric.shadows;
+
+import static android.os.Build.VERSION_CODES.R;
+
+import android.graphics.animation.RenderNodeAnimator;
+import org.robolectric.annotation.Implementation;
+import org.robolectric.annotation.Implements;
+import org.robolectric.nativeruntime.DefaultNativeRuntimeLoader;
+import org.robolectric.nativeruntime.RenderNodeAnimatorNatives;
+import org.robolectric.shadows.ShadowNativeRenderNodeAnimator.Picker;
+
+/** Shadow for {@link RenderNodeAnimator} that is backed by native code */
+@Implements(
+    value = RenderNodeAnimator.class,
+    minSdk = R,
+    shadowPicker = Picker.class,
+    isInAndroidSdk = false)
+public class ShadowNativeRenderNodeAnimator {
+  @Implementation
+  protected static long nCreateAnimator(int property, float finalValue) {
+    DefaultNativeRuntimeLoader.injectAndLoad();
+    return RenderNodeAnimatorNatives.nCreateAnimator(property, finalValue);
+  }
+
+  @Implementation
+  protected static long nCreateCanvasPropertyFloatAnimator(long canvasProperty, float finalValue) {
+    DefaultNativeRuntimeLoader.injectAndLoad();
+    return RenderNodeAnimatorNatives.nCreateCanvasPropertyFloatAnimator(canvasProperty, finalValue);
+  }
+
+  @Implementation
+  protected static long nCreateCanvasPropertyPaintAnimator(
+      long canvasProperty, int paintField, float finalValue) {
+    DefaultNativeRuntimeLoader.injectAndLoad();
+    return RenderNodeAnimatorNatives.nCreateCanvasPropertyPaintAnimator(
+        canvasProperty, paintField, finalValue);
+  }
+
+  @Implementation
+  protected static long nCreateRevealAnimator(int x, int y, float startRadius, float endRadius) {
+    DefaultNativeRuntimeLoader.injectAndLoad();
+    return RenderNodeAnimatorNatives.nCreateRevealAnimator(x, y, startRadius, endRadius);
+  }
+
+  @Implementation
+  protected static void nSetStartValue(long nativePtr, float startValue) {
+    RenderNodeAnimatorNatives.nSetStartValue(nativePtr, startValue);
+  }
+
+  @Implementation
+  protected static void nSetDuration(long nativePtr, long duration) {
+    RenderNodeAnimatorNatives.nSetDuration(nativePtr, duration);
+  }
+
+  @Implementation
+  protected static long nGetDuration(long nativePtr) {
+    return RenderNodeAnimatorNatives.nGetDuration(nativePtr);
+  }
+
+  @Implementation
+  protected static void nSetStartDelay(long nativePtr, long startDelay) {
+    RenderNodeAnimatorNatives.nSetStartDelay(nativePtr, startDelay);
+  }
+
+  @Implementation
+  protected static void nSetInterpolator(long animPtr, long interpolatorPtr) {
+    RenderNodeAnimatorNatives.nSetInterpolator(animPtr, interpolatorPtr);
+  }
+
+  @Implementation
+  protected static void nSetAllowRunningAsync(long animPtr, boolean mayRunAsync) {
+    RenderNodeAnimatorNatives.nSetAllowRunningAsync(animPtr, mayRunAsync);
+  }
+
+  @Implementation
+  protected static void nSetListener(long animPtr, RenderNodeAnimator listener) {
+    RenderNodeAnimatorNatives.nSetListener(animPtr, listener);
+  }
+
+  @Implementation
+  protected static void nStart(long animPtr) {
+    RenderNodeAnimatorNatives.nStart(animPtr);
+  }
+
+  @Implementation
+  protected static void nEnd(long animPtr) {
+    RenderNodeAnimatorNatives.nEnd(animPtr);
+  }
+
+  /** Shadow picker for {@link RenderNodeAnimator}. */
+  public static final class Picker extends GraphicsShadowPicker<Object> {
+    public Picker() {
+      super(ShadowRenderNodeAnimatorR.class, ShadowNativeRenderNodeAnimator.class);
+    }
+  }
+}
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeRenderNodeAnimatorQ.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeRenderNodeAnimatorQ.java
new file mode 100644
index 0000000..f833782
--- /dev/null
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeRenderNodeAnimatorQ.java
@@ -0,0 +1,100 @@
+package org.robolectric.shadows;
+
+import static android.os.Build.VERSION_CODES.O;
+import static android.os.Build.VERSION_CODES.Q;
+
+import org.robolectric.annotation.Implementation;
+import org.robolectric.annotation.Implements;
+import org.robolectric.nativeruntime.DefaultNativeRuntimeLoader;
+import org.robolectric.nativeruntime.RenderNodeAnimatorNatives;
+import org.robolectric.shadows.ShadowNativeRenderNodeAnimatorQ.Picker;
+
+/**
+ * Shadow for {@link android.view.RenderNodeAnimator} for Android Q and below that is backed by
+ * native code
+ */
+@Implements(
+    className = "android.view.RenderNodeAnimator",
+    minSdk = O,
+    maxSdk = Q,
+    looseSignatures = true,
+    shadowPicker = Picker.class)
+public class ShadowNativeRenderNodeAnimatorQ {
+  @Implementation
+  protected static long nCreateAnimator(int property, float finalValue) {
+    DefaultNativeRuntimeLoader.injectAndLoad();
+    return RenderNodeAnimatorNatives.nCreateAnimator(property, finalValue);
+  }
+
+  @Implementation
+  protected static long nCreateCanvasPropertyFloatAnimator(long canvasProperty, float finalValue) {
+    DefaultNativeRuntimeLoader.injectAndLoad();
+    return RenderNodeAnimatorNatives.nCreateCanvasPropertyFloatAnimator(canvasProperty, finalValue);
+  }
+
+  @Implementation
+  protected static long nCreateCanvasPropertyPaintAnimator(
+      long canvasProperty, int paintField, float finalValue) {
+    DefaultNativeRuntimeLoader.injectAndLoad();
+    return RenderNodeAnimatorNatives.nCreateCanvasPropertyPaintAnimator(
+        canvasProperty, paintField, finalValue);
+  }
+
+  @Implementation
+  protected static long nCreateRevealAnimator(int x, int y, float startRadius, float endRadius) {
+    DefaultNativeRuntimeLoader.injectAndLoad();
+    return RenderNodeAnimatorNatives.nCreateRevealAnimator(x, y, startRadius, endRadius);
+  }
+
+  @Implementation
+  protected static void nSetStartValue(long nativePtr, float startValue) {
+    RenderNodeAnimatorNatives.nSetStartValue(nativePtr, startValue);
+  }
+
+  @Implementation
+  protected static void nSetDuration(long nativePtr, long duration) {
+    RenderNodeAnimatorNatives.nSetDuration(nativePtr, duration);
+  }
+
+  @Implementation
+  protected static long nGetDuration(long nativePtr) {
+    return RenderNodeAnimatorNatives.nGetDuration(nativePtr);
+  }
+
+  @Implementation
+  protected static void nSetStartDelay(long nativePtr, long startDelay) {
+    RenderNodeAnimatorNatives.nSetStartDelay(nativePtr, startDelay);
+  }
+
+  @Implementation
+  protected static void nSetInterpolator(long animPtr, long interpolatorPtr) {
+    RenderNodeAnimatorNatives.nSetInterpolator(animPtr, interpolatorPtr);
+  }
+
+  @Implementation
+  protected static void nSetAllowRunningAsync(long animPtr, boolean mayRunAsync) {
+    RenderNodeAnimatorNatives.nSetAllowRunningAsync(animPtr, mayRunAsync);
+  }
+
+  @Implementation
+  protected static void nSetListener(Object animPtr, Object listener) {
+    RenderNodeAnimatorNatives.nSetListener((long) animPtr, listener);
+  }
+
+  @Implementation
+  protected static void nStart(long animPtr) {
+    RenderNodeAnimatorNatives.nStart(animPtr);
+  }
+
+  @Implementation
+  protected static void nEnd(long animPtr) {
+    RenderNodeAnimatorNatives.nEnd(animPtr);
+  }
+
+  /** Shadow picker for {@link android.view.RenderNodeAnimator}. */
+  public static final class Picker extends GraphicsShadowPicker<Object> {
+    public Picker() {
+      super(ShadowRenderNodeAnimator.class, ShadowNativeRenderNodeAnimatorQ.class);
+    }
+  }
+}
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeRenderNodeOP.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeRenderNodeOP.java
new file mode 100644
index 0000000..f17650e
--- /dev/null
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeRenderNodeOP.java
@@ -0,0 +1,465 @@
+package org.robolectric.shadows;
+
+import static android.os.Build.VERSION_CODES.O;
+import static android.os.Build.VERSION_CODES.P;
+import static org.robolectric.util.reflector.Reflector.reflector;
+
+import android.graphics.Canvas;
+import org.robolectric.annotation.Implementation;
+import org.robolectric.annotation.Implements;
+import org.robolectric.annotation.RealObject;
+import org.robolectric.nativeruntime.DefaultNativeRuntimeLoader;
+import org.robolectric.nativeruntime.RenderNodeNatives;
+import org.robolectric.shadows.ShadowNativeRenderNodeOP.Picker;
+import org.robolectric.util.reflector.Accessor;
+import org.robolectric.util.reflector.ForType;
+
+/** Shadow for {@link android.view.RenderNode} that is backed by native code */
+@Implements(
+    className = "android.view.RenderNode",
+    minSdk = O,
+    maxSdk = P,
+    looseSignatures = true,
+    shadowPicker = Picker.class)
+public class ShadowNativeRenderNodeOP {
+  @RealObject Object realRenderNode;
+
+  @Implementation
+  protected static long nCreate(String name) {
+    DefaultNativeRuntimeLoader.injectAndLoad();
+    return RenderNodeNatives.nCreate(name);
+  }
+
+  @Implementation
+  protected static long nGetNativeFinalizer() {
+    return RenderNodeNatives.nGetNativeFinalizer();
+  }
+
+  @Implementation
+  protected static void nOutput(long renderNode) {
+    RenderNodeNatives.nOutput(renderNode);
+  }
+
+  @Implementation
+  protected static void nAddAnimator(long renderNode, long animatorPtr) {
+    RenderNodeNatives.nAddAnimator(renderNode, animatorPtr);
+  }
+
+  @Implementation
+  protected static void nEndAllAnimators(long renderNode) {
+    RenderNodeNatives.nEndAllAnimators(renderNode);
+  }
+
+  @Implementation
+  protected static boolean nIsValid(long renderNode) {
+    return RenderNodeNatives.nIsValid(renderNode);
+  }
+
+  @Implementation
+  protected static void nGetTransformMatrix(long renderNode, long nativeMatrix) {
+    RenderNodeNatives.nGetTransformMatrix(renderNode, nativeMatrix);
+  }
+
+  @Implementation
+  protected static void nGetInverseTransformMatrix(long renderNode, long nativeMatrix) {
+    RenderNodeNatives.nGetInverseTransformMatrix(renderNode, nativeMatrix);
+  }
+
+  @Implementation
+  protected static boolean nHasIdentityMatrix(long renderNode) {
+    return RenderNodeNatives.nHasIdentityMatrix(renderNode);
+  }
+
+  @Implementation
+  protected static boolean nOffsetTopAndBottom(long renderNode, int offset) {
+    return RenderNodeNatives.nOffsetTopAndBottom(renderNode, offset);
+  }
+
+  @Implementation
+  protected static boolean nOffsetLeftAndRight(long renderNode, int offset) {
+    return RenderNodeNatives.nOffsetLeftAndRight(renderNode, offset);
+  }
+
+  @Implementation
+  protected static boolean nSetLeftTopRightBottom(
+      long renderNode, int left, int top, int right, int bottom) {
+    return RenderNodeNatives.nSetLeftTopRightBottom(renderNode, left, top, right, bottom);
+  }
+
+  @Implementation
+  protected static boolean nSetLeft(long renderNode, int left) {
+    return RenderNodeNatives.nSetLeft(renderNode, left);
+  }
+
+  @Implementation
+  protected static boolean nSetTop(long renderNode, int top) {
+    return RenderNodeNatives.nSetTop(renderNode, top);
+  }
+
+  @Implementation
+  protected static boolean nSetRight(long renderNode, int right) {
+    return RenderNodeNatives.nSetRight(renderNode, right);
+  }
+
+  @Implementation
+  protected static boolean nSetBottom(long renderNode, int bottom) {
+    return RenderNodeNatives.nSetBottom(renderNode, bottom);
+  }
+
+  @Implementation
+  protected static int nGetLeft(long renderNode) {
+    return RenderNodeNatives.nGetLeft(renderNode);
+  }
+
+  @Implementation
+  protected static int nGetTop(long renderNode) {
+    return RenderNodeNatives.nGetTop(renderNode);
+  }
+
+  @Implementation
+  protected static int nGetRight(long renderNode) {
+    return RenderNodeNatives.nGetRight(renderNode);
+  }
+
+  @Implementation
+  protected static int nGetBottom(long renderNode) {
+    return RenderNodeNatives.nGetBottom(renderNode);
+  }
+
+  @Implementation
+  protected static boolean nSetCameraDistance(long renderNode, float distance) {
+    return RenderNodeNatives.nSetCameraDistance(renderNode, distance);
+  }
+
+  @Implementation
+  protected static boolean nSetPivotY(long renderNode, float pivotY) {
+    return RenderNodeNatives.nSetPivotY(renderNode, pivotY);
+  }
+
+  @Implementation
+  protected static boolean nSetPivotX(long renderNode, float pivotX) {
+    return RenderNodeNatives.nSetPivotX(renderNode, pivotX);
+  }
+
+  @Implementation
+  protected static boolean nResetPivot(long renderNode) {
+    return RenderNodeNatives.nResetPivot(renderNode);
+  }
+
+  @Implementation
+  protected static boolean nSetLayerType(long renderNode, int layerType) {
+    return RenderNodeNatives.nSetLayerType(renderNode, layerType);
+  }
+
+  @Implementation
+  protected static int nGetLayerType(long renderNode) {
+    return RenderNodeNatives.nGetLayerType(renderNode);
+  }
+
+  @Implementation
+  protected static boolean nSetLayerPaint(long renderNode, long paint) {
+    return RenderNodeNatives.nSetLayerPaint(renderNode, paint);
+  }
+
+  @Implementation
+  protected static boolean nSetClipToBounds(long renderNode, boolean clipToBounds) {
+    return RenderNodeNatives.nSetClipToBounds(renderNode, clipToBounds);
+  }
+
+  @Implementation
+  protected static boolean nGetClipToBounds(long renderNode) {
+    return RenderNodeNatives.nGetClipToBounds(renderNode);
+  }
+
+  @Implementation
+  protected static boolean nSetClipBounds(
+      long renderNode, int left, int top, int right, int bottom) {
+    return RenderNodeNatives.nSetClipBounds(renderNode, left, top, right, bottom);
+  }
+
+  @Implementation
+  protected static boolean nSetClipBoundsEmpty(long renderNode) {
+    return RenderNodeNatives.nSetClipBoundsEmpty(renderNode);
+  }
+
+  @Implementation
+  protected static boolean nSetProjectBackwards(long renderNode, boolean shouldProject) {
+    return RenderNodeNatives.nSetProjectBackwards(renderNode, shouldProject);
+  }
+
+  @Implementation
+  protected static boolean nSetProjectionReceiver(long renderNode, boolean shouldReceive) {
+    return RenderNodeNatives.nSetProjectionReceiver(renderNode, shouldReceive);
+  }
+
+  @Implementation
+  protected static boolean nSetOutlineRoundRect(
+      long renderNode, int left, int top, int right, int bottom, float radius, float alpha) {
+    return RenderNodeNatives.nSetOutlineRoundRect(
+        renderNode, left, top, right, bottom, radius, alpha);
+  }
+
+  @Implementation
+  protected static boolean nSetOutlineEmpty(long renderNode) {
+    return RenderNodeNatives.nSetOutlineEmpty(renderNode);
+  }
+
+  @Implementation
+  protected static boolean nSetOutlineNone(long renderNode) {
+    return RenderNodeNatives.nSetOutlineNone(renderNode);
+  }
+
+  @Implementation
+  protected static boolean nHasShadow(long renderNode) {
+    return RenderNodeNatives.nHasShadow(renderNode);
+  }
+
+  @Implementation
+  protected static boolean nSetSpotShadowColor(long renderNode, int color) {
+    return RenderNodeNatives.nSetSpotShadowColor(renderNode, color);
+  }
+
+  @Implementation
+  protected static boolean nSetAmbientShadowColor(long renderNode, int color) {
+    return RenderNodeNatives.nSetAmbientShadowColor(renderNode, color);
+  }
+
+  @Implementation
+  protected static int nGetSpotShadowColor(long renderNode) {
+    return RenderNodeNatives.nGetSpotShadowColor(renderNode);
+  }
+
+  @Implementation
+  protected static int nGetAmbientShadowColor(long renderNode) {
+    return RenderNodeNatives.nGetAmbientShadowColor(renderNode);
+  }
+
+  @Implementation
+  protected static boolean nSetClipToOutline(long renderNode, boolean clipToOutline) {
+    return RenderNodeNatives.nSetClipToOutline(renderNode, clipToOutline);
+  }
+
+  @Implementation
+  protected static boolean nSetRevealClip(
+      long renderNode, boolean shouldClip, float x, float y, float radius) {
+    return RenderNodeNatives.nSetRevealClip(renderNode, shouldClip, x, y, radius);
+  }
+
+  @Implementation
+  protected static boolean nSetAlpha(long renderNode, float alpha) {
+    return RenderNodeNatives.nSetAlpha(renderNode, alpha);
+  }
+
+  @Implementation
+  protected static boolean nSetHasOverlappingRendering(
+      long renderNode, boolean hasOverlappingRendering) {
+    return RenderNodeNatives.nSetHasOverlappingRendering(renderNode, hasOverlappingRendering);
+  }
+
+  @Implementation
+  protected static void nSetUsageHint(long renderNode, int usageHint) {
+    RenderNodeNatives.nSetUsageHint(renderNode, usageHint);
+  }
+
+  @Implementation
+  protected static boolean nSetElevation(long renderNode, float lift) {
+    return RenderNodeNatives.nSetElevation(renderNode, lift);
+  }
+
+  @Implementation
+  protected static boolean nSetTranslationX(long renderNode, float translationX) {
+    return RenderNodeNatives.nSetTranslationX(renderNode, translationX);
+  }
+
+  @Implementation
+  protected static boolean nSetTranslationY(long renderNode, float translationY) {
+    return RenderNodeNatives.nSetTranslationY(renderNode, translationY);
+  }
+
+  @Implementation
+  protected static boolean nSetTranslationZ(long renderNode, float translationZ) {
+    return RenderNodeNatives.nSetTranslationZ(renderNode, translationZ);
+  }
+
+  @Implementation
+  protected static boolean nSetRotation(long renderNode, float rotation) {
+    return RenderNodeNatives.nSetRotation(renderNode, rotation);
+  }
+
+  @Implementation
+  protected static boolean nSetRotationX(long renderNode, float rotationX) {
+    return RenderNodeNatives.nSetRotationX(renderNode, rotationX);
+  }
+
+  @Implementation
+  protected static boolean nSetRotationY(long renderNode, float rotationY) {
+    return RenderNodeNatives.nSetRotationY(renderNode, rotationY);
+  }
+
+  @Implementation
+  protected static boolean nSetScaleX(long renderNode, float scaleX) {
+    return RenderNodeNatives.nSetScaleX(renderNode, scaleX);
+  }
+
+  @Implementation
+  protected static boolean nSetScaleY(long renderNode, float scaleY) {
+    return RenderNodeNatives.nSetScaleY(renderNode, scaleY);
+  }
+
+  @Implementation
+  protected static boolean nSetStaticMatrix(long renderNode, long nativeMatrix) {
+    return RenderNodeNatives.nSetStaticMatrix(renderNode, nativeMatrix);
+  }
+
+  @Implementation
+  protected static boolean nSetAnimationMatrix(long renderNode, long animationMatrix) {
+    return RenderNodeNatives.nSetAnimationMatrix(renderNode, animationMatrix);
+  }
+
+  @Implementation
+  protected static boolean nHasOverlappingRendering(long renderNode) {
+    return RenderNodeNatives.nHasOverlappingRendering(renderNode);
+  }
+
+  @Implementation
+  protected static boolean nGetAnimationMatrix(long renderNode, long animationMatrix) {
+    return RenderNodeNatives.nGetAnimationMatrix(renderNode, animationMatrix);
+  }
+
+  @Implementation
+  protected static boolean nGetClipToOutline(long renderNode) {
+    return RenderNodeNatives.nGetClipToOutline(renderNode);
+  }
+
+  @Implementation
+  protected static float nGetAlpha(long renderNode) {
+    return RenderNodeNatives.nGetAlpha(renderNode);
+  }
+
+  @Implementation
+  protected static float nGetCameraDistance(long renderNode) {
+    return RenderNodeNatives.nGetCameraDistance(renderNode);
+  }
+
+  @Implementation
+  protected static float nGetScaleX(long renderNode) {
+    return RenderNodeNatives.nGetScaleX(renderNode);
+  }
+
+  @Implementation
+  protected static float nGetScaleY(long renderNode) {
+    return RenderNodeNatives.nGetScaleY(renderNode);
+  }
+
+  @Implementation
+  protected static float nGetElevation(long renderNode) {
+    return RenderNodeNatives.nGetElevation(renderNode);
+  }
+
+  @Implementation
+  protected static float nGetTranslationX(long renderNode) {
+    return RenderNodeNatives.nGetTranslationX(renderNode);
+  }
+
+  @Implementation
+  protected static float nGetTranslationY(long renderNode) {
+    return RenderNodeNatives.nGetTranslationY(renderNode);
+  }
+
+  @Implementation
+  protected static float nGetTranslationZ(long renderNode) {
+    return RenderNodeNatives.nGetTranslationZ(renderNode);
+  }
+
+  @Implementation
+  protected static float nGetRotation(long renderNode) {
+    return RenderNodeNatives.nGetRotation(renderNode);
+  }
+
+  @Implementation
+  protected static float nGetRotationX(long renderNode) {
+    return RenderNodeNatives.nGetRotationX(renderNode);
+  }
+
+  @Implementation
+  protected static float nGetRotationY(long renderNode) {
+    return RenderNodeNatives.nGetRotationY(renderNode);
+  }
+
+  @Implementation
+  protected static boolean nIsPivotExplicitlySet(long renderNode) {
+    return RenderNodeNatives.nIsPivotExplicitlySet(renderNode);
+  }
+
+  @Implementation
+  protected static float nGetPivotX(long renderNode) {
+    return RenderNodeNatives.nGetPivotX(renderNode);
+  }
+
+  @Implementation
+  protected static float nGetPivotY(long renderNode) {
+    return RenderNodeNatives.nGetPivotY(renderNode);
+  }
+
+  @Implementation
+  protected static int nGetWidth(long renderNode) {
+    return RenderNodeNatives.nGetWidth(renderNode);
+  }
+
+  @Implementation
+  protected static int nGetHeight(long renderNode) {
+    return RenderNodeNatives.nGetHeight(renderNode);
+  }
+
+  @Implementation
+  protected static boolean nSetAllowForceDark(long renderNode, boolean allowForceDark) {
+    return RenderNodeNatives.nSetAllowForceDark(renderNode, allowForceDark);
+  }
+
+  @Implementation
+  protected static boolean nGetAllowForceDark(long renderNode) {
+    return RenderNodeNatives.nGetAllowForceDark(renderNode);
+  }
+
+  @Implementation
+  protected static long nGetUniqueId(long renderNode) {
+    return RenderNodeNatives.nGetUniqueId(renderNode);
+  }
+
+  // In APIs Q+, RenderNodes are used to maintain DisplayLists instead of through DisplayListCanvas.
+  // In APIs O-P, this function would call the version of nFinishRecording that didn't use a
+  // RenderNode at all and instead returned a DisplayList that would need to be moved.
+  // To bridge the two implementations, the end(..) function here uses the API Q+ version so that
+  // the RenderNode is marked as valid when isValid() is called.
+  @Implementation
+  protected void end(Object canvas) {
+    long nativeRenderNode =
+        reflector(RenderNodeOpReflector.class, realRenderNode).getNativeRenderNode();
+    long nativeCanvasWrapper = reflector(CanvasReflector.class, canvas).getNativeCanvasWrapper();
+    ShadowNativeRecordingCanvas.nFinishRecording(nativeCanvasWrapper, nativeRenderNode);
+    reflector(DisplayListCanvasReflector.class, canvas).recycle();
+  }
+
+  @ForType(className = "android.view.RenderNode")
+  interface RenderNodeOpReflector {
+    @Accessor("mNativeRenderNode")
+    long getNativeRenderNode();
+  }
+
+  @ForType(className = "android.view.DisplayListCanvas")
+  interface DisplayListCanvasReflector {
+    void recycle();
+  }
+
+  @ForType(Canvas.class)
+  interface CanvasReflector {
+    long getNativeCanvasWrapper();
+  }
+
+  /** Shadow picker for {@link android.view.RenderNode}. */
+  public static final class Picker extends GraphicsShadowPicker<Object> {
+    public Picker() {
+      super(ShadowRenderNode.class, ShadowNativeRenderNodeOP.class);
+    }
+  }
+}
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeRuntimeShader.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeRuntimeShader.java
new file mode 100644
index 0000000..9793d1d
--- /dev/null
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeRuntimeShader.java
@@ -0,0 +1,180 @@
+package org.robolectric.shadows;
+
+import static android.os.Build.VERSION_CODES.O;
+import static android.os.Build.VERSION_CODES.R;
+import static android.os.Build.VERSION_CODES.S;
+import static android.os.Build.VERSION_CODES.S_V2;
+import static android.os.Build.VERSION_CODES.TIRAMISU;
+
+import android.graphics.RuntimeShader;
+import org.robolectric.annotation.Implementation;
+import org.robolectric.annotation.Implements;
+import org.robolectric.annotation.RealObject;
+import org.robolectric.nativeruntime.DefaultNativeRuntimeLoader;
+import org.robolectric.nativeruntime.RuntimeShaderNatives;
+import org.robolectric.shadow.api.Shadow;
+import org.robolectric.shadows.ShadowNativeRuntimeShader.Picker;
+import org.robolectric.util.ReflectionHelpers.ClassParameter;
+
+/** Shadow for {@link RuntimeShader} that is backed by native code */
+@Implements(value = RuntimeShader.class, minSdk = O, shadowPicker = Picker.class)
+public class ShadowNativeRuntimeShader {
+
+  @RealObject RuntimeShader runtimeShader;
+
+  private static final String RIPPLE_SHADER_UNIFORMS_31 =
+      "uniform vec2 in_origin;\n"
+          + "uniform vec2 in_touch;\n"
+          + "uniform float in_progress;\n"
+          + "uniform float in_maxRadius;\n"
+          + "uniform vec2 in_resolutionScale;\n"
+          + "uniform vec2 in_noiseScale;\n"
+          + "uniform float in_hasMask;\n"
+          + "uniform float in_noisePhase;\n"
+          + "uniform float in_turbulencePhase;\n"
+          + "uniform vec2 in_tCircle1;\n"
+          + "uniform vec2 in_tCircle2;\n"
+          + "uniform vec2 in_tCircle3;\n"
+          + "uniform vec2 in_tRotation1;\n"
+          + "uniform vec2 in_tRotation2;\n"
+          + "uniform vec2 in_tRotation3;\n"
+          + "uniform vec4 in_color;\n"
+          + "uniform vec4 in_sparkleColor;\n"
+          + "uniform shader in_shader;\n";
+  private static final String RIPPLE_SHADER_LIB_31 =
+      "float triangleNoise(vec2 n) {\n"
+          + "  n  = fract(n * vec2(5.3987, 5.4421));\n"
+          + "  n += dot(n.yx, n.xy + vec2(21.5351, 14.3137));\n"
+          + "  float xy = n.x * n.y;\n"
+          + "  return fract(xy * 95.4307) + fract(xy * 75.04961) - 1.0;\n"
+          + "}"
+          + "const float PI = 3.1415926535897932384626;\n"
+          + "\n"
+          + "float threshold(float v, float l, float h) {\n"
+          + "    return step(l, v) * (1.0 - step(h, v));\n"
+          + "}\n"
+          + "float sparkles(vec2 uv, float t) {\n"
+          + "  float n = triangleNoise(uv);\n"
+          + "  float s = 0.0;\n"
+          + "  for (float i = 0; i < 4; i += 1) {\n"
+          + "    float l = i * 0.1;\n"
+          + "    float h = l + 0.05;\n"
+          + "    float o = sin(PI * (t + 0.35 * i));\n"
+          + "    s += threshold(n + o, l, h);\n"
+          + "  }\n"
+          + "  return saturate(s) * in_sparkleColor.a;\n"
+          + "}\n"
+          + "float softCircle(vec2 uv, vec2 xy, float radius, float blur) {\n"
+          + "  float blurHalf = blur * 0.5;\n"
+          + "  float d = distance(uv, xy);\n"
+          + "  return 1. - smoothstep(1. - blurHalf, 1. + blurHalf, d / radius);\n"
+          + "}\n"
+          + "float softRing(vec2 uv, vec2 xy, float radius, float progress, float blur) {\n"
+          + "  float thickness = 0.05 * radius;\n"
+          + "  float currentRadius = radius * progress;\n"
+          + "  float circle_outer = softCircle(uv, xy, currentRadius + thickness, blur);\n"
+          + "  float circle_inner = softCircle(uv, xy, max(currentRadius - thickness, 0.), "
+          + "    blur);\n"
+          + "  return saturate(circle_outer - circle_inner);\n"
+          + "}\n"
+          + "float subProgress(float start, float end, float progress) {\n"
+          + "    float sub = clamp(progress, start, end);\n"
+          + "    return (sub - start) / (end - start); \n"
+          + "}\n"
+          + "mat2 rotate2d(vec2 rad){\n"
+          + "  return mat2(rad.x, -rad.y, rad.y, rad.x);\n"
+          + "}\n"
+          + "float circle_grid(vec2 resolution, vec2 coord, float time, vec2 center,\n"
+          + "    vec2 rotation, float cell_diameter) {\n"
+          + "  coord = rotate2d(rotation) * (center - coord) + center;\n"
+          + "  coord = mod(coord, cell_diameter) / resolution;\n"
+          + "  float normal_radius = cell_diameter / resolution.y * 0.5;\n"
+          + "  float radius = 0.65 * normal_radius;\n"
+          + "  return softCircle(coord, vec2(normal_radius), radius, radius * 50.0);\n"
+          + "}\n"
+          + "float turbulence(vec2 uv, float t) {\n"
+          + "  const vec2 scale = vec2(0.8);\n"
+          + "  uv = uv * scale;\n"
+          + "  float g1 = circle_grid(scale, uv, t, in_tCircle1, in_tRotation1, 0.17);\n"
+          + "  float g2 = circle_grid(scale, uv, t, in_tCircle2, in_tRotation2, 0.2);\n"
+          + "  float g3 = circle_grid(scale, uv, t, in_tCircle3, in_tRotation3, 0.275);\n"
+          + "  float v = (g1 * g1 + g2 - g3) * 0.5;\n"
+          + "  return saturate(0.45 + 0.8 * v);\n"
+          + "}\n";
+  private static final String RIPPLE_SHADER_MAIN_31 =
+      "vec4 main(vec2 p) {\n"
+          + "    float fadeIn = subProgress(0., 0.13, in_progress);\n"
+          + "    float scaleIn = subProgress(0., 1.0, in_progress);\n"
+          + "    float fadeOutNoise = subProgress(0.4, 0.5, in_progress);\n"
+          + "    float fadeOutRipple = subProgress(0.4, 1., in_progress);\n"
+          + "    vec2 center = mix(in_touch, in_origin, saturate(in_progress * 2.0));\n"
+          + "    float ring = softRing(p, center, in_maxRadius, scaleIn, 1.);\n"
+          + "    float alpha = min(fadeIn, 1. - fadeOutNoise);\n"
+          + "    vec2 uv = p * in_resolutionScale;\n"
+          + "    vec2 densityUv = uv - mod(uv, in_noiseScale);\n"
+          + "    float turbulence = turbulence(uv, in_turbulencePhase);\n"
+          + "    float sparkleAlpha = sparkles(densityUv, in_noisePhase) * ring * alpha "
+          + "* turbulence;\n"
+          + "    float fade = min(fadeIn, 1. - fadeOutRipple);\n"
+          + "    float waveAlpha = softCircle(p, center, in_maxRadius * scaleIn, 1.) * fade "
+          + "* in_color.a;\n"
+          + "    vec4 waveColor = vec4(in_color.rgb * waveAlpha, waveAlpha);\n"
+          + "    vec4 sparkleColor = vec4(in_sparkleColor.rgb * in_sparkleColor.a, "
+          + "in_sparkleColor.a);\n"
+          + "    float mask = in_hasMask == 1. ? sample(in_shader, p).a > 0. ? 1. : 0. : 1.;\n"
+          + "    return mix(waveColor, sparkleColor, sparkleAlpha) * mask;\n"
+          + "}";
+  private static final String RIPPLE_SHADER_31 =
+      RIPPLE_SHADER_UNIFORMS_31 + RIPPLE_SHADER_LIB_31 + RIPPLE_SHADER_MAIN_31;
+
+  @Implementation(minSdk = TIRAMISU)
+  protected void __constructor__(String sksl) {
+    // This is a workaround for supporting RippleShader from T+ with the native code from S.
+    // There were some new capabilities added to SKSL in T which are not available in S. Use the
+    // RippleShader SKSL from T in S.
+    // TODO(hoisie): Delete this shadow method when RNG is updated to use native libraries from T+.
+    try {
+      if (Class.forName("android.graphics.drawable.RippleShader").isInstance(runtimeShader)) {
+        sksl = RIPPLE_SHADER_31;
+      }
+    } catch (ClassNotFoundException e) {
+      throw new AssertionError(e);
+    }
+    Shadow.invokeConstructor(
+        RuntimeShader.class, runtimeShader, ClassParameter.from(String.class, sksl));
+  }
+
+  @Implementation(minSdk = R)
+  protected static long nativeGetFinalizer() {
+    return RuntimeShaderNatives.nativeGetFinalizer();
+  }
+
+  @Implementation(minSdk = S)
+  protected static long nativeCreateBuilder(String sksl) {
+    DefaultNativeRuntimeLoader.injectAndLoad();
+    return RuntimeShaderNatives.nativeCreateBuilder(sksl);
+  }
+
+  @Implementation(minSdk = S, maxSdk = S_V2)
+  protected static long nativeCreateShader(long shaderBuilder, long matrix, boolean isOpaque) {
+    return RuntimeShaderNatives.nativeCreateShader(shaderBuilder, matrix, isOpaque);
+  }
+
+  @Implementation(minSdk = S, maxSdk = S_V2)
+  protected static void nativeUpdateUniforms(
+      long shaderBuilder, String uniformName, float[] uniforms) {
+    RuntimeShaderNatives.nativeUpdateUniforms(shaderBuilder, uniformName, uniforms);
+  }
+
+  @Implementation(minSdk = S)
+  protected static void nativeUpdateShader(long shaderBuilder, String shaderName, long shader) {
+    RuntimeShaderNatives.nativeUpdateShader(shaderBuilder, shaderName, shader);
+  }
+
+  /** Shadow picker for {@link RuntimeShader}. */
+  public static final class Picker extends GraphicsShadowPicker<Object> {
+    public Picker() {
+      super(null, ShadowNativeRuntimeShader.class);
+    }
+  }
+}
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeShader.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeShader.java
new file mode 100644
index 0000000..1a216f8
--- /dev/null
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeShader.java
@@ -0,0 +1,28 @@
+package org.robolectric.shadows;
+
+import static android.os.Build.VERSION_CODES.O;
+
+import android.graphics.Shader;
+import org.robolectric.annotation.Implementation;
+import org.robolectric.annotation.Implements;
+import org.robolectric.nativeruntime.DefaultNativeRuntimeLoader;
+import org.robolectric.nativeruntime.ShaderNatives;
+import org.robolectric.shadows.ShadowNativeShader.Picker;
+
+/** Shadow for {@link Shader} that is backed by native code */
+@Implements(value = Shader.class, minSdk = O, shadowPicker = Picker.class)
+public class ShadowNativeShader {
+
+  @Implementation(minSdk = O)
+  protected static long nativeGetFinalizer() {
+    DefaultNativeRuntimeLoader.injectAndLoad();
+    return ShaderNatives.nativeGetFinalizer();
+  }
+
+  /** Shadow picker for {@link Shader}. */
+  public static final class Picker extends GraphicsShadowPicker<Object> {
+    public Picker() {
+      super(null, ShadowNativeShader.class);
+    }
+  }
+}
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeStaticLayout.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeStaticLayout.java
new file mode 100644
index 0000000..04504d3
--- /dev/null
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeStaticLayout.java
@@ -0,0 +1,330 @@
+package org.robolectric.shadows;
+
+import static android.os.Build.VERSION_CODES.O;
+import static android.os.Build.VERSION_CODES.O_MR1;
+import static android.os.Build.VERSION_CODES.P;
+import static org.robolectric.util.reflector.Reflector.reflector;
+
+import android.graphics.Paint;
+import android.text.StaticLayout;
+import android.text.TextPaint;
+import java.nio.ByteBuffer;
+import java.util.Locale;
+import org.robolectric.annotation.Implementation;
+import org.robolectric.annotation.Implements;
+import org.robolectric.nativeruntime.DefaultNativeRuntimeLoader;
+import org.robolectric.nativeruntime.LineBreakerNatives;
+import org.robolectric.nativeruntime.MeasuredTextBuilderNatives;
+import org.robolectric.nativeruntime.MeasuredTextNatives;
+import org.robolectric.nativeruntime.NativeAllocationRegistryNatives;
+import org.robolectric.res.android.NativeObjRegistry;
+import org.robolectric.shadows.ShadowNativeStaticLayout.Picker;
+import org.robolectric.util.reflector.Accessor;
+import org.robolectric.util.reflector.ForType;
+
+/**
+ * Shadow for {@link StaticLayout} that is backed by native code for Android O-P. In Android Q, the
+ * native methods relate to text layout were heavily refactored and moved to MeasuredText and
+ * LineBreaker.
+ */
+@Implements(
+    value = StaticLayout.class,
+    minSdk = O,
+    maxSdk = P,
+    looseSignatures = true,
+    shadowPicker = Picker.class)
+public class ShadowNativeStaticLayout {
+
+  // Only used for the O/O_MR1 adapter logic.
+  static final NativeObjRegistry<NativeStaticLayoutSetup> nativeObjectRegistry =
+      new NativeObjRegistry<>(NativeStaticLayoutSetup.class);
+
+  @Implementation(minSdk = P, maxSdk = P)
+  protected static long nInit(
+      int breakStrategy,
+      int hyphenationFrequency,
+      boolean isJustified,
+      int[] indents,
+      int[] leftPaddings,
+      int[] rightPaddings) {
+    DefaultNativeRuntimeLoader.injectAndLoad();
+    return LineBreakerNatives.nInit(breakStrategy, hyphenationFrequency, isJustified, indents);
+  }
+
+  @Implementation(minSdk = P, maxSdk = P)
+  protected static void nFinish(long nativePtr) {
+    LineBreakerNatives.nFinishP(nativePtr);
+  }
+
+  /**
+   * This has to use looseSignatures due to {@code recycle} param with non-public type {@code
+   * android.text.StaticLayout$LineBreaks}.
+   */
+  @Implementation(minSdk = P, maxSdk = P)
+  protected static int nComputeLineBreaks(
+      Object nativePtr,
+      Object text,
+      Object measuredTextPtr,
+      Object length,
+      Object firstWidth,
+      Object firstWidthLineCount,
+      Object restWidth,
+      Object variableTabStopsObject,
+      Object defaultTabStop,
+      Object indentsOffset,
+      Object recycle,
+      Object recycleLength,
+      Object recycleBreaks,
+      Object recycleWidths,
+      Object recycleAscents,
+      Object recycleDescents,
+      Object recycleFlags,
+      Object charWidths) {
+
+    return LineBreakerNatives.nComputeLineBreaksP(
+        (long) nativePtr,
+        (char[]) text,
+        (long) measuredTextPtr,
+        (int) length,
+        (float) firstWidth,
+        (int) firstWidthLineCount,
+        (float) restWidth,
+        intsToFloat((int[]) variableTabStopsObject),
+        ((Number) defaultTabStop).floatValue(),
+        (int) indentsOffset,
+        recycle,
+        (int) recycleLength,
+        (int[]) recycleBreaks,
+        (float[]) recycleWidths,
+        (float[]) recycleAscents,
+        (float[]) recycleDescents,
+        (int[]) recycleFlags,
+        (float[]) charWidths);
+  }
+
+  @Implementation(minSdk = O, maxSdk = O_MR1)
+  protected static long nNewBuilder() {
+    return nativeObjectRegistry.register(new NativeStaticLayoutSetup());
+  }
+
+  @Implementation(minSdk = O, maxSdk = O_MR1)
+  protected static void nFreeBuilder(long nativePtr) {
+    NativeStaticLayoutSetup setup = nativeObjectRegistry.getNativeObject(nativePtr);
+
+    NativeAllocationRegistryNatives.applyFreeFunction(
+        LineBreakerNatives.nGetReleaseResultFunc(), setup.lineBreakerResultPtr);
+
+    nativeObjectRegistry.unregister(nativePtr);
+  }
+
+  @Implementation(minSdk = O, maxSdk = O_MR1)
+  protected static void nFinishBuilder(long nativePtr) {
+    // No-op
+  }
+
+  @Implementation(minSdk = O, maxSdk = O_MR1)
+  protected static long nLoadHyphenator(ByteBuffer buf, int offset, int minPrefix, int minSuffix) {
+    // nLoadHyphenator is not supported
+    return 0;
+  }
+
+  @Implementation(minSdk = O, maxSdk = O_MR1)
+  protected static void nSetLocale(long nativePtr, String locale, long nativeHyphenator) {
+    NativeStaticLayoutSetup setup = nativeObjectRegistry.getNativeObject(nativePtr);
+    setup.localePaint.setTextLocale(Locale.forLanguageTag(locale));
+  }
+
+  @Implementation(minSdk = O, maxSdk = O_MR1)
+  protected static void nSetIndents(long nativePtr, int[] indents) {
+    NativeStaticLayoutSetup setup = nativeObjectRegistry.getNativeObject(nativePtr);
+    setup.indents = indents;
+  }
+
+  @Implementation(minSdk = O, maxSdk = O_MR1)
+  protected static void nSetupParagraph(
+      long nativePtr,
+      char[] text,
+      int length,
+      float firstWidth,
+      int firstWidthLineCount,
+      float restWidth,
+      int[] variableTabStops,
+      int defaultTabStop,
+      int breakStrategy,
+      int hyphenationFrequency,
+      boolean isJustified) {
+    NativeStaticLayoutSetup setup = nativeObjectRegistry.getNativeObject(nativePtr);
+    setup.text = text;
+    setup.length = length;
+    setup.firstWidth = firstWidth;
+    setup.firstWidthLineCount = firstWidthLineCount;
+    setup.restWidth = restWidth;
+    setup.variableTabStops = variableTabStops;
+    setup.defaultTabStop = defaultTabStop;
+    setup.breakStrategy = breakStrategy;
+    setup.hyphenationFrequency = hyphenationFrequency;
+    setup.isJustified = isJustified;
+    setup.measuredTextBuilderPtr = MeasuredTextBuilderNatives.nInitBuilder();
+  }
+
+  @Implementation(minSdk = O, maxSdk = O_MR1)
+  protected static float nAddStyleRun(
+      long nativePtr, long nativePaint, long nativeTypeface, int start, int end, boolean isRtl) {
+    NativeStaticLayoutSetup setup = nativeObjectRegistry.getNativeObject(nativePtr);
+
+    MeasuredTextBuilderNatives.nAddStyleRun(
+        setup.measuredTextBuilderPtr, nativePaint, start, end, isRtl);
+    return 0f;
+  }
+
+  @Implementation
+  protected static void nAddMeasuredRun(long nativePtr, int start, int end, float[] widths) {
+    NativeStaticLayoutSetup setup = nativeObjectRegistry.getNativeObject(nativePtr);
+    MeasuredTextBuilderNatives.nAddStyleRun(
+        setup.measuredTextBuilderPtr, setup.localePaint.getNativeInstance(), start, end, false);
+  }
+
+  @Implementation
+  protected static void nAddReplacementRun(long nativePtr, int start, int end, float width) {
+    NativeStaticLayoutSetup setup = nativeObjectRegistry.getNativeObject(nativePtr);
+    MeasuredTextBuilderNatives.nAddReplacementRun(
+        setup.measuredTextBuilderPtr, setup.localePaint.getNativeInstance(), start, end, width);
+  }
+
+  @Implementation
+  protected static void nGetWidths(long nativePtr, float[] widths) {
+    // Returns the width of each char in the text.
+    NativeStaticLayoutSetup setup = nativeObjectRegistry.getNativeObject(nativePtr);
+    setup.measuredTextPtr =
+        MeasuredTextBuilderNatives.nBuildMeasuredText(
+            setup.measuredTextBuilderPtr, 0, setup.text, false, false);
+    for (int i = 0; i < setup.text.length; i++) {
+      widths[i] = MeasuredTextNatives.nGetCharWidthAt(setup.measuredTextPtr, i);
+    }
+    MeasuredTextBuilderNatives.nFreeBuilder(setup.measuredTextBuilderPtr);
+  }
+
+  /**
+   * This has to use looseSignatures due to {@code recycle} param with non-public type {@code
+   * android.text.StaticLayout$LineBreaks}.
+   */
+  @Implementation
+  protected static int nComputeLineBreaks(
+      Object /*long*/ nativePtr,
+      Object /*LineBreaks*/ recycle,
+      Object /*int[]*/ recycleBreaksObject,
+      Object /*float[]*/ recycleWidthsObject,
+      Object /*int[]*/ recycleFlagsObject,
+      Object /*int*/ recycleLength) {
+
+    int[] recycleBreaks = (int[]) recycleBreaksObject;
+    float[] recycleWidths = (float[]) recycleWidthsObject;
+    int[] recycleFlags = (int[]) recycleFlagsObject;
+
+    NativeStaticLayoutSetup setup = nativeObjectRegistry.getNativeObject((long) nativePtr);
+
+    long lineBreakerBuilderPtr =
+        LineBreakerNatives.nInit(
+            setup.breakStrategy, setup.hyphenationFrequency, setup.isJustified, setup.indents);
+
+    setup.lineBreakerResultPtr =
+        LineBreakerNatives.nComputeLineBreaks(
+            lineBreakerBuilderPtr,
+            setup.text,
+            setup.measuredTextPtr,
+            setup.length,
+            setup.firstWidth,
+            setup.firstWidthLineCount,
+            setup.restWidth,
+            intsToFloat(setup.variableTabStops),
+            (float) setup.defaultTabStop,
+            0);
+
+    int lineCount = LineBreakerNatives.nGetLineCount(setup.lineBreakerResultPtr);
+
+    if (lineCount > recycleBreaks.length) {
+      // resize the recycle objects
+      recycleBreaks = new int[lineCount];
+      recycleWidths = new float[lineCount];
+      recycleFlags = new int[lineCount];
+      reflector(LineBreaksReflector.class, recycle).setBreaks(recycleBreaks);
+      reflector(LineBreaksReflector.class, recycle).setWidths(recycleWidths);
+      reflector(LineBreaksReflector.class, recycle).setFlags(recycleFlags);
+    }
+
+    for (int i = 0; i < lineCount; i++) {
+      recycleBreaks[i] = LineBreakerNatives.nGetLineBreakOffset(setup.lineBreakerResultPtr, i);
+      recycleWidths[i] = LineBreakerNatives.nGetLineWidth(setup.lineBreakerResultPtr, i);
+      recycleFlags[i] = LineBreakerNatives.nGetLineFlag(setup.lineBreakerResultPtr, i);
+    }
+
+    // Release the pointers used for the builder, the result pointer is the only relevant pointer
+    // now.
+    NativeAllocationRegistryNatives.applyFreeFunction(
+        LineBreakerNatives.nGetReleaseFunc(), lineBreakerBuilderPtr);
+
+    NativeAllocationRegistryNatives.applyFreeFunction(
+        MeasuredTextNatives.nGetReleaseFunc(), setup.measuredTextPtr);
+
+    return lineCount;
+  }
+
+  static final class NativeStaticLayoutSetup {
+
+    char[] text;
+    int length;
+    float firstWidth;
+    int firstWidthLineCount;
+    float restWidth;
+    int[] variableTabStops;
+    int defaultTabStop;
+    int breakStrategy;
+    int hyphenationFrequency;
+    boolean isJustified;
+    int[] indents;
+    Paint localePaint = new TextPaint(); // TODO(hoisie): use `mPaint` from StaticLayout.Builder
+    long measuredTextBuilderPtr;
+    long measuredTextPtr;
+    long lineBreakerResultPtr;
+  }
+
+  private static float[] intsToFloat(int[] intArray) {
+    if (intArray == null) {
+      return null;
+    }
+    float[] floatArray = new float[intArray.length];
+
+    for (int i = 0; i < floatArray.length; i++) {
+      floatArray[i] = intArray[i];
+    }
+    return floatArray;
+  }
+
+  @ForType(className = "android.text.StaticLayout$LineBreaks")
+  interface LineBreaksReflector {
+    @Accessor("breaks")
+    int[] getBreaks();
+
+    @Accessor("breaks")
+    void setBreaks(int[] breaks);
+
+    @Accessor("widths")
+    float[] getWidths();
+
+    @Accessor("widths")
+    void setWidths(float[] widths);
+
+    @Accessor("flags")
+    int[] getFlags();
+
+    @Accessor("flags")
+    void setFlags(int[] flags);
+  }
+
+  /** Shadow picker for {@link StaticLayout}. */
+  public static final class Picker extends GraphicsShadowPicker<Object> {
+    public Picker() {
+      super(ShadowStaticLayout.class, ShadowNativeStaticLayout.class);
+    }
+  }
+}
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeSumPathEffect.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeSumPathEffect.java
new file mode 100644
index 0000000..2d436ae
--- /dev/null
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeSumPathEffect.java
@@ -0,0 +1,28 @@
+package org.robolectric.shadows;
+
+import static android.os.Build.VERSION_CODES.O;
+
+import android.graphics.SumPathEffect;
+import org.robolectric.annotation.Implementation;
+import org.robolectric.annotation.Implements;
+import org.robolectric.nativeruntime.DefaultNativeRuntimeLoader;
+import org.robolectric.nativeruntime.SumPathEffectNatives;
+import org.robolectric.shadows.ShadowNativeSumPathEffect.Picker;
+
+/** Shadow for {@link SumPathEffect} that is backed by native code */
+@Implements(value = SumPathEffect.class, minSdk = O, shadowPicker = Picker.class)
+public class ShadowNativeSumPathEffect {
+
+  @Implementation(minSdk = O)
+  protected static long nativeCreate(long first, long second) {
+    DefaultNativeRuntimeLoader.injectAndLoad();
+    return SumPathEffectNatives.nativeCreate(first, second);
+  }
+
+  /** Shadow picker for {@link SumPathEffect}. */
+  public static final class Picker extends GraphicsShadowPicker<Object> {
+    public Picker() {
+      super(null, ShadowNativeSumPathEffect.class);
+    }
+  }
+}
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeSurface.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeSurface.java
new file mode 100644
index 0000000..fe78998
--- /dev/null
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeSurface.java
@@ -0,0 +1,147 @@
+package org.robolectric.shadows;
+
+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 android.os.Build.VERSION_CODES.S;
+
+import android.graphics.Canvas;
+import android.graphics.Rect;
+import android.graphics.SurfaceTexture;
+import android.hardware.HardwareBuffer;
+import android.os.Parcel;
+import android.view.Surface;
+import android.view.Surface.OutOfResourcesException;
+import org.robolectric.annotation.Implementation;
+import org.robolectric.annotation.Implements;
+import org.robolectric.nativeruntime.DefaultNativeRuntimeLoader;
+import org.robolectric.nativeruntime.SurfaceNatives;
+import org.robolectric.shadows.ShadowNativeSurface.Picker;
+
+/** Shadow for {@link Surface} that is backed by native code */
+@Implements(value = Surface.class, minSdk = O, shadowPicker = Picker.class, isInAndroidSdk = false)
+public class ShadowNativeSurface {
+  @Implementation
+  protected static long nativeCreateFromSurfaceTexture(SurfaceTexture surfaceTexture)
+      throws OutOfResourcesException {
+    DefaultNativeRuntimeLoader.injectAndLoad();
+    return SurfaceNatives.nativeCreateFromSurfaceTexture(surfaceTexture);
+  }
+
+  @Implementation
+  protected static long nativeCreateFromSurfaceControl(long surfaceControlNativeObject) {
+    DefaultNativeRuntimeLoader.injectAndLoad();
+    return SurfaceNatives.nativeCreateFromSurfaceControl(surfaceControlNativeObject);
+  }
+
+  @Implementation(minSdk = Q)
+  protected static long nativeGetFromSurfaceControl(
+      long surfaceObject, long surfaceControlNativeObject) {
+    DefaultNativeRuntimeLoader.injectAndLoad();
+    return SurfaceNatives.nativeGetFromSurfaceControl(surfaceObject, surfaceControlNativeObject);
+  }
+
+  @Implementation(minSdk = S)
+  protected static long nativeGetFromBlastBufferQueue(
+      long surfaceObject, long blastBufferQueueNativeObject) {
+    return SurfaceNatives.nativeGetFromBlastBufferQueue(
+        surfaceObject, blastBufferQueueNativeObject);
+  }
+
+  @Implementation
+  protected static long nativeLockCanvas(long nativeObject, Canvas canvas, Rect dirty)
+      throws OutOfResourcesException {
+    return SurfaceNatives.nativeLockCanvas(nativeObject, canvas, dirty);
+  }
+
+  @Implementation
+  protected static void nativeUnlockCanvasAndPost(long nativeObject, Canvas canvas) {
+    SurfaceNatives.nativeUnlockCanvasAndPost(nativeObject, canvas);
+  }
+
+  @Implementation
+  protected static void nativeRelease(long nativeObject) {
+    SurfaceNatives.nativeRelease(nativeObject);
+  }
+
+  @Implementation
+  protected static boolean nativeIsValid(long nativeObject) {
+    return SurfaceNatives.nativeIsValid(nativeObject);
+  }
+
+  @Implementation
+  protected static boolean nativeIsConsumerRunningBehind(long nativeObject) {
+    return SurfaceNatives.nativeIsConsumerRunningBehind(nativeObject);
+  }
+
+  @Implementation
+  protected static long nativeReadFromParcel(long nativeObject, Parcel source) {
+    return SurfaceNatives.nativeReadFromParcel(nativeObject, source);
+  }
+
+  @Implementation
+  protected static void nativeWriteToParcel(long nativeObject, Parcel dest) {
+    SurfaceNatives.nativeWriteToParcel(nativeObject, dest);
+  }
+
+  @Implementation
+  protected static void nativeAllocateBuffers(long nativeObject) {
+    SurfaceNatives.nativeAllocateBuffers(nativeObject);
+  }
+
+  @Implementation
+  protected static int nativeGetWidth(long nativeObject) {
+    return SurfaceNatives.nativeGetWidth(nativeObject);
+  }
+
+  @Implementation
+  protected static int nativeGetHeight(long nativeObject) {
+    return SurfaceNatives.nativeGetHeight(nativeObject);
+  }
+
+  @Implementation
+  protected static long nativeGetNextFrameNumber(long nativeObject) {
+    return SurfaceNatives.nativeGetNextFrameNumber(nativeObject);
+  }
+
+  @Implementation
+  protected static int nativeSetScalingMode(long nativeObject, int scalingMode) {
+    return SurfaceNatives.nativeSetScalingMode(nativeObject, scalingMode);
+  }
+
+  @Implementation
+  protected static int nativeForceScopedDisconnect(long nativeObject) {
+    return SurfaceNatives.nativeForceScopedDisconnect(nativeObject);
+  }
+
+  @Implementation(minSdk = S)
+  protected static int nativeAttachAndQueueBufferWithColorSpace(
+      long nativeObject, HardwareBuffer buffer, int colorSpaceId) {
+    return SurfaceNatives.nativeAttachAndQueueBufferWithColorSpace(
+        nativeObject, buffer, colorSpaceId);
+  }
+
+  @Implementation(minSdk = O_MR1)
+  protected static int nativeSetSharedBufferModeEnabled(long nativeObject, boolean enabled) {
+    return SurfaceNatives.nativeSetSharedBufferModeEnabled(nativeObject, enabled);
+  }
+
+  @Implementation(minSdk = O_MR1)
+  protected static int nativeSetAutoRefreshEnabled(long nativeObject, boolean enabled) {
+    return SurfaceNatives.nativeSetAutoRefreshEnabled(nativeObject, enabled);
+  }
+
+  @Implementation(minSdk = S)
+  protected static int nativeSetFrameRate(
+      long nativeObject, float frameRate, int compatibility, int changeFrameRateStrategy) {
+    return SurfaceNatives.nativeSetFrameRate(
+        nativeObject, frameRate, compatibility, changeFrameRateStrategy);
+  }
+
+  /** Shadow picker for {@link Surface}. */
+  public static final class Picker extends GraphicsShadowPicker<Object> {
+    public Picker() {
+      super(ShadowSurface.class, ShadowNativeSurface.class);
+    }
+  }
+}
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeSweepGradient.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeSweepGradient.java
new file mode 100644
index 0000000..d51300f
--- /dev/null
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeSweepGradient.java
@@ -0,0 +1,44 @@
+package org.robolectric.shadows;
+
+import static android.os.Build.VERSION_CODES.O;
+import static android.os.Build.VERSION_CODES.P;
+import static android.os.Build.VERSION_CODES.Q;
+
+import android.graphics.SweepGradient;
+import org.robolectric.annotation.Implementation;
+import org.robolectric.annotation.Implements;
+import org.robolectric.nativeruntime.DefaultNativeRuntimeLoader;
+import org.robolectric.nativeruntime.SweepGradientNatives;
+import org.robolectric.shadows.ShadowNativeSweepGradient.Picker;
+
+/** Shadow for {@link SweepGradient} that is backed by native code */
+@Implements(value = SweepGradient.class, minSdk = O, shadowPicker = Picker.class)
+public class ShadowNativeSweepGradient {
+
+  @Implementation(minSdk = Q)
+  protected static long nativeCreate(
+      long matrix, float x, float y, long[] colors, float[] positions, long colorSpaceHandle) {
+    DefaultNativeRuntimeLoader.injectAndLoad();
+    return SweepGradientNatives.nativeCreate(matrix, x, y, colors, positions, colorSpaceHandle);
+  }
+
+  @Implementation(minSdk = O, maxSdk = P)
+  protected static long nativeCreate1(
+      long matrix, float x, float y, int[] colors, float[] positions) {
+    DefaultNativeRuntimeLoader.injectAndLoad();
+    return SweepGradientNatives.nativeCreate1(matrix, x, y, colors, positions);
+  }
+
+  @Implementation(minSdk = O, maxSdk = P)
+  protected static long nativeCreate2(long matrix, float x, float y, int color0, int color1) {
+    DefaultNativeRuntimeLoader.injectAndLoad();
+    return SweepGradientNatives.nativeCreate2(matrix, x, y, color0, color1);
+  }
+
+  /** Shadow picker for {@link SweepGradient}. */
+  public static final class Picker extends GraphicsShadowPicker<Object> {
+    public Picker() {
+      super(null, ShadowNativeSweepGradient.class);
+    }
+  }
+}
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeSystemFonts.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeSystemFonts.java
new file mode 100644
index 0000000..f154df3
--- /dev/null
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeSystemFonts.java
@@ -0,0 +1,125 @@
+package org.robolectric.shadows;
+
+import static android.os.Build.VERSION_CODES.Q;
+import static android.os.Build.VERSION_CODES.S;
+import static org.robolectric.util.reflector.Reflector.reflector;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.graphics.fonts.Font;
+import android.graphics.fonts.FontCustomizationParser;
+import android.graphics.fonts.FontFamily;
+import android.graphics.fonts.SystemFonts;
+import android.os.Build;
+import android.os.Build.VERSION_CODES;
+import android.text.FontConfig;
+import android.util.ArrayMap;
+import android.util.Log;
+import com.google.common.base.Preconditions;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.channels.FileChannel;
+import java.util.ArrayList;
+import java.util.Map;
+import org.robolectric.annotation.Implementation;
+import org.robolectric.annotation.Implements;
+import org.robolectric.shadows.ShadowNativeSystemFonts.Picker;
+import org.robolectric.util.reflector.Direct;
+import org.robolectric.util.reflector.ForType;
+import org.robolectric.util.reflector.Static;
+
+/**
+ * Shadow for {@link SystemFonts} for the Robolectric native runtime. It supports getting system
+ * font config using a custom fonts path.
+ */
+@Implements(
+    value = SystemFonts.class,
+    minSdk = Build.VERSION_CODES.Q,
+    isInAndroidSdk = false,
+    shadowPicker = Picker.class)
+public class ShadowNativeSystemFonts {
+  @Implementation(minSdk = S)
+  protected static FontConfig getSystemFontConfigInternal(
+      String fontsXml,
+      String systemFontDir,
+      String oemXml,
+      String productFontDir,
+      Map<String, File> updatableFontMap,
+      long lastModifiedDate,
+      int configVersion) {
+    String fontDir = System.getProperty("robolectric.nativeruntime.fontdir");
+    Preconditions.checkNotNull(fontDir);
+    Preconditions.checkState(new File(fontDir).isDirectory(), "Missing fonts directory");
+    Preconditions.checkState(fontDir.endsWith("/"), "Fonts directory must end with a slash");
+    return reflector(SystemFontsReflector.class)
+        .getSystemFontConfigInternal(
+            fontDir + "fonts.xml",
+            fontDir,
+            null,
+            null,
+            updatableFontMap,
+            lastModifiedDate,
+            configVersion);
+  }
+
+  @Implementation(maxSdk = VERSION_CODES.R)
+  public static FontConfig.Alias[] buildSystemFallback(
+      String xmlPath,
+      String systemFontDir,
+      FontCustomizationParser.Result oemCustomization,
+      ArrayMap<String, FontFamily[]> fallbackMap,
+      ArrayList<Font> availableFonts) {
+    String fontDir = System.getProperty("robolectric.nativeruntime.fontdir");
+    Preconditions.checkNotNull(fontDir);
+    Preconditions.checkState(new File(fontDir).isDirectory(), "Missing fonts directory");
+    Preconditions.checkState(fontDir.endsWith("/"), "Fonts directory must end with a slash");
+    return reflector(SystemFontsReflector.class)
+        .buildSystemFallback(
+            fontDir + "fonts.xml", fontDir, oemCustomization, fallbackMap, availableFonts);
+  }
+
+  @Implementation(minSdk = Q, maxSdk = Q)
+  @Nullable
+  protected static ByteBuffer mmap(@NonNull String fullPath) {
+    try (FileInputStream file = new FileInputStream(fullPath)) {
+      final FileChannel fileChannel = file.getChannel();
+      final long fontSize = fileChannel.size();
+      return fileChannel.map(FileChannel.MapMode.READ_ONLY, 0, fontSize);
+    } catch (IOException e) {
+      Log.w("SystemFonts", e.getMessage());
+      return null;
+    }
+  }
+
+  @ForType(SystemFonts.class)
+  interface SystemFontsReflector {
+    @Static
+    @Direct
+    FontConfig getSystemFontConfigInternal(
+        String fontsXml,
+        String systemFontDir,
+        String oemXml,
+        String productFontDir,
+        Map<String, File> updatableFontMap,
+        long lastModifiedDate,
+        int configVersion);
+
+    @Static
+    @Direct
+    FontConfig.Alias[] buildSystemFallback(
+        String xmlPath,
+        String fontDir,
+        FontCustomizationParser.Result oemCustomization,
+        ArrayMap<String, FontFamily[]> fallbackMap,
+        ArrayList<Font> availableFonts);
+  }
+
+  /** Shadow picker for {@link SystemFonts}. */
+  public static final class Picker extends GraphicsShadowPicker<Object> {
+    public Picker() {
+      super(null, ShadowNativeSystemFonts.class);
+    }
+  }
+}
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeTableMaskFilter.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeTableMaskFilter.java
new file mode 100644
index 0000000..7d7c0a3
--- /dev/null
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeTableMaskFilter.java
@@ -0,0 +1,44 @@
+package org.robolectric.shadows;
+
+import static android.os.Build.VERSION_CODES.O;
+
+import android.graphics.TableMaskFilter;
+import org.robolectric.annotation.Implementation;
+import org.robolectric.annotation.Implements;
+import org.robolectric.nativeruntime.DefaultNativeRuntimeLoader;
+import org.robolectric.nativeruntime.TableMaskFilterNatives;
+import org.robolectric.shadows.ShadowNativeTableMaskFilter.Picker;
+
+/** Shadow for {@link TableMaskFilter} that is backed by native code */
+@Implements(
+    value = TableMaskFilter.class,
+    minSdk = O,
+    shadowPicker = Picker.class,
+    isInAndroidSdk = false)
+public class ShadowNativeTableMaskFilter {
+
+  @Implementation(minSdk = O)
+  protected static long nativeNewTable(byte[] table) {
+    DefaultNativeRuntimeLoader.injectAndLoad();
+    return TableMaskFilterNatives.nativeNewTable(table);
+  }
+
+  @Implementation(minSdk = O)
+  protected static long nativeNewClip(int min, int max) {
+    DefaultNativeRuntimeLoader.injectAndLoad();
+    return TableMaskFilterNatives.nativeNewClip(min, max);
+  }
+
+  @Implementation(minSdk = O)
+  protected static long nativeNewGamma(float gamma) {
+    DefaultNativeRuntimeLoader.injectAndLoad();
+    return TableMaskFilterNatives.nativeNewGamma(gamma);
+  }
+
+  /** Shadow picker for {@link TableMaskFilter}. */
+  public static final class Picker extends GraphicsShadowPicker<Object> {
+    public Picker() {
+      super(null, ShadowNativeTableMaskFilter.class);
+    }
+  }
+}
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeThreadedRenderer.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeThreadedRenderer.java
new file mode 100644
index 0000000..e9ea464
--- /dev/null
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeThreadedRenderer.java
@@ -0,0 +1,183 @@
+package org.robolectric.shadows;
+
+import static android.os.Build.VERSION_CODES.O;
+import static android.os.Build.VERSION_CODES.P;
+
+import android.graphics.Bitmap;
+import android.view.ThreadedRenderer;
+import org.robolectric.annotation.Implementation;
+import org.robolectric.annotation.Implements;
+import org.robolectric.nativeruntime.DefaultNativeRuntimeLoader;
+import org.robolectric.nativeruntime.HardwareRendererNatives;
+import org.robolectric.shadows.ShadowNativeThreadedRenderer.Picker;
+
+/** Shadow for {@link ThreadedRenderer} that is backed by native code */
+@Implements(value = ThreadedRenderer.class, minSdk = O, maxSdk = P, shadowPicker = Picker.class)
+public class ShadowNativeThreadedRenderer {
+
+  // ThreadedRenderer specific functions. These do not exist in HardwareRenderer
+  @Implementation
+  protected static boolean nSupportsOpenGL() {
+    return false;
+  }
+
+  // HardwareRenderer methods. These exist in both ThreadedRenderer and HardwareRenderer.
+  @Implementation
+  protected static void nRotateProcessStatsBuffer() {
+    HardwareRendererNatives.nRotateProcessStatsBuffer();
+  }
+
+  @Implementation
+  protected static void nSetProcessStatsBuffer(int fd) {
+    HardwareRendererNatives.nSetProcessStatsBuffer(fd);
+  }
+
+  @Implementation
+  protected static int nGetRenderThreadTid(long nativeProxy) {
+    return HardwareRendererNatives.nGetRenderThreadTid(nativeProxy);
+  }
+
+  @Implementation
+  protected static long nCreateRootRenderNode() {
+    DefaultNativeRuntimeLoader.injectAndLoad();
+    return HardwareRendererNatives.nCreateRootRenderNode();
+  }
+
+  @Implementation
+  protected static long nCreateProxy(boolean translucent, long rootRenderNode) {
+    return HardwareRendererNatives.nCreateProxy(translucent, rootRenderNode);
+  }
+
+  @Implementation
+  protected static void nDeleteProxy(long nativeProxy) {
+    HardwareRendererNatives.nDeleteProxy(nativeProxy);
+  }
+
+  @Implementation
+  protected static boolean nLoadSystemProperties(long nativeProxy) {
+    return HardwareRendererNatives.nLoadSystemProperties(nativeProxy);
+  }
+
+  @Implementation
+  protected static void nSetName(long nativeProxy, String name) {
+    HardwareRendererNatives.nSetName(nativeProxy, name);
+  }
+
+  @Implementation
+  protected static void nSetStopped(long nativeProxy, boolean stopped) {
+    HardwareRendererNatives.nSetStopped(nativeProxy, stopped);
+  }
+
+  @Implementation
+  protected static void nSetOpaque(long nativeProxy, boolean opaque) {
+    HardwareRendererNatives.nSetOpaque(nativeProxy, opaque);
+  }
+
+  @Implementation
+  protected static int nSyncAndDrawFrame(long nativeProxy, long[] frameInfo, int size) {
+    return HardwareRendererNatives.nSyncAndDrawFrame(nativeProxy, frameInfo, size);
+  }
+
+  @Implementation
+  protected static void nDestroy(long nativeProxy, long rootRenderNode) {
+    HardwareRendererNatives.nDestroy(nativeProxy, rootRenderNode);
+  }
+
+  @Implementation
+  protected static void nRegisterAnimatingRenderNode(long rootRenderNode, long animatingNode) {
+    HardwareRendererNatives.nRegisterAnimatingRenderNode(rootRenderNode, animatingNode);
+  }
+
+  @Implementation
+  protected static void nRegisterVectorDrawableAnimator(long rootRenderNode, long animator) {
+    HardwareRendererNatives.nRegisterVectorDrawableAnimator(rootRenderNode, animator);
+  }
+
+  @Implementation
+  protected static long nCreateTextureLayer(long nativeProxy) {
+    return HardwareRendererNatives.nCreateTextureLayer(nativeProxy);
+  }
+
+  @Implementation
+  protected static void nBuildLayer(long nativeProxy, long node) {
+    HardwareRendererNatives.nBuildLayer(nativeProxy, node);
+  }
+
+  @Implementation
+  protected static void nPushLayerUpdate(long nativeProxy, long layer) {
+    HardwareRendererNatives.nPushLayerUpdate(nativeProxy, layer);
+  }
+
+  @Implementation
+  protected static void nCancelLayerUpdate(long nativeProxy, long layer) {
+    HardwareRendererNatives.nCancelLayerUpdate(nativeProxy, layer);
+  }
+
+  @Implementation
+  protected static void nDetachSurfaceTexture(long nativeProxy, long layer) {
+    HardwareRendererNatives.nDetachSurfaceTexture(nativeProxy, layer);
+  }
+
+  @Implementation
+  protected static void nDestroyHardwareResources(long nativeProxy) {
+    HardwareRendererNatives.nDestroyHardwareResources(nativeProxy);
+  }
+
+  @Implementation
+  protected static void nTrimMemory(int level) {
+    HardwareRendererNatives.nTrimMemory(level);
+  }
+
+  @Implementation
+  protected static void nOverrideProperty(String name, String value) {
+    HardwareRendererNatives.nOverrideProperty(name, value);
+  }
+
+  @Implementation
+  protected static void nFence(long nativeProxy) {
+    HardwareRendererNatives.nFence(nativeProxy);
+  }
+
+  @Implementation
+  protected static void nStopDrawing(long nativeProxy) {
+    HardwareRendererNatives.nStopDrawing(nativeProxy);
+  }
+
+  @Implementation
+  protected static void nNotifyFramePending(long nativeProxy) {
+    HardwareRendererNatives.nNotifyFramePending(nativeProxy);
+  }
+
+  @Implementation
+  protected static void nAddRenderNode(long nativeProxy, long rootRenderNode, boolean placeFront) {
+    HardwareRendererNatives.nAddRenderNode(nativeProxy, rootRenderNode, placeFront);
+  }
+
+  @Implementation
+  protected static void nRemoveRenderNode(long nativeProxy, long rootRenderNode) {
+    HardwareRendererNatives.nRemoveRenderNode(nativeProxy, rootRenderNode);
+  }
+
+  @Implementation
+  protected static void nDrawRenderNode(long nativeProxy, long rootRenderNode) {
+    HardwareRendererNatives.nDrawRenderNode(nativeProxy, rootRenderNode);
+  }
+
+  @Implementation
+  protected static void nSetContentDrawBounds(
+      long nativeProxy, int left, int top, int right, int bottom) {
+    HardwareRendererNatives.nSetContentDrawBounds(nativeProxy, left, top, right, bottom);
+  }
+
+  @Implementation
+  protected static Bitmap nCreateHardwareBitmap(long renderNode, int width, int height) {
+    return HardwareRendererNatives.nCreateHardwareBitmap(renderNode, width, height);
+  }
+
+  /** Shadow picker for {@link ThreadedRenderer}. */
+  public static final class Picker extends GraphicsShadowPicker<Object> {
+    public Picker() {
+      super(ShadowThreadedRenderer.class, ShadowNativeThreadedRenderer.class);
+    }
+  }
+}
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeTypeface.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeTypeface.java
new file mode 100644
index 0000000..0c7fcf6
--- /dev/null
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeTypeface.java
@@ -0,0 +1,291 @@
+package org.robolectric.shadows;
+
+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.P;
+import static android.os.Build.VERSION_CODES.Q;
+import static android.os.Build.VERSION_CODES.R;
+import static android.os.Build.VERSION_CODES.S;
+import static android.os.Build.VERSION_CODES.TIRAMISU;
+import static org.robolectric.util.reflector.Reflector.reflector;
+
+import android.graphics.FontFamily;
+import android.graphics.Typeface;
+import android.graphics.fonts.FontVariationAxis;
+import android.text.FontConfig;
+import android.util.ArrayMap;
+import android.util.Log;
+import com.google.common.base.Preconditions;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.channels.FileChannel;
+import java.util.List;
+import java.util.Map;
+import org.robolectric.annotation.Implementation;
+import org.robolectric.annotation.Implements;
+import org.robolectric.nativeruntime.DefaultNativeRuntimeLoader;
+import org.robolectric.nativeruntime.TypefaceNatives;
+import org.robolectric.shadow.api.Shadow;
+import org.robolectric.util.reflector.Direct;
+import org.robolectric.util.reflector.ForType;
+import org.robolectric.util.reflector.Static;
+
+/** Shadow for {@link Typeface} that is backed by native code */
+@Implements(value = Typeface.class, looseSignatures = true, minSdk = O, isInAndroidSdk = false)
+public class ShadowNativeTypeface extends ShadowTypeface {
+
+  private static final String TAG = "ShadowNativeTypeface";
+
+  // Style value for building typeface.
+  private static final int STYLE_NORMAL = 0;
+  private static final int STYLE_ITALIC = 1;
+
+  @Implementation(minSdk = S)
+  protected static void __staticInitializer__() {
+    Shadow.directInitialize(Typeface.class);
+    // Initialize the system font map. In real Android this is done as part of Application startup
+    // and uses a more complex SharedMemory system not supported in Robolectric.
+    Typeface.loadPreinstalledSystemFontMap();
+  }
+
+  @Implementation(minSdk = P, maxSdk = P)
+  protected static void buildSystemFallback(
+      String xmlPath,
+      String systemFontDir,
+      ArrayMap<String, Typeface> fontMap,
+      ArrayMap<String, FontFamily[]> fallbackMap) {
+    String fontDir = System.getProperty("robolectric.nativeruntime.fontdir");
+    Preconditions.checkNotNull(fontDir);
+    Preconditions.checkState(new File(fontDir).isDirectory(), "Missing fonts directory");
+    Preconditions.checkState(fontDir.endsWith("/"), "Fonts directory must end with a slash");
+    reflector(TypefaceReflector.class)
+        .buildSystemFallback(fontDir + "fonts.xml", fontDir, fontMap, fallbackMap);
+  }
+
+  @Implementation(minSdk = O, maxSdk = O_MR1)
+  protected static File getSystemFontConfigLocation() {
+    // Ensure that the Robolectric native runtime is loaded in ordere to ensure that the
+    // `robolectric.nativeruntime.fontdir` system property is valid.
+    DefaultNativeRuntimeLoader.injectAndLoad();
+    String fontDir = System.getProperty("robolectric.nativeruntime.fontdir");
+    Preconditions.checkNotNull(fontDir);
+    Preconditions.checkState(new File(fontDir).isDirectory(), "Missing fonts directory");
+    Preconditions.checkState(fontDir.endsWith("/"), "Fonts directory must end with a slash");
+    return new File(fontDir);
+  }
+
+  @SuppressWarnings("unchecked")
+  @Implementation(minSdk = O, maxSdk = O_MR1)
+  protected static Object makeFamilyFromParsed(Object family, Object bufferForPathMap) {
+    FontConfigFamilyReflector reflector = reflector(FontConfigFamilyReflector.class, family);
+    Map<String, ByteBuffer> bufferForPath = (Map<String, ByteBuffer>) bufferForPathMap;
+
+    FontFamily fontFamily =
+        Shadow.newInstance(
+            FontFamily.class,
+            new Class<?>[] {String.class, int.class},
+            new Object[] {reflector.getLanguage(), reflector.getVariant()});
+    for (FontConfig.Font font : reflector.getFonts()) {
+      String fullPathName =
+          System.getProperty("robolectric.nativeruntime.fontdir")
+              + reflector(FontConfigFontReflector.class, font).getFontName();
+      ByteBuffer fontBuffer = bufferForPath.get(fullPathName);
+      if (fontBuffer == null) {
+        try (FileInputStream file = new FileInputStream(fullPathName)) {
+          FileChannel fileChannel = file.getChannel();
+          long fontSize = fileChannel.size();
+          fontBuffer = fileChannel.map(FileChannel.MapMode.READ_ONLY, 0, fontSize);
+          bufferForPath.put(fullPathName, fontBuffer);
+        } catch (IOException e) {
+          Log.w(TAG, "Error mapping font file " + fullPathName);
+          continue;
+        }
+      }
+      if (!fontFamily.addFontFromBuffer(
+          fontBuffer,
+          font.getTtcIndex(),
+          font.getAxes(),
+          font.getWeight(),
+          font.isItalic() ? STYLE_ITALIC : STYLE_NORMAL)) {
+        Log.e(TAG, "Error creating font " + fullPathName + "#" + font.getTtcIndex());
+      }
+    }
+    if (!fontFamily.freeze()) {
+      // Treat as system error since reaching here means that a system pre-installed font
+      // can't be used by our font stack.
+      Log.w(TAG, "Unable to load Family: " + reflector.getName() + ":" + reflector.getLanguage());
+      return null;
+    }
+    return fontFamily;
+  }
+
+  @Implementation(minSdk = LOLLIPOP)
+  protected static long nativeCreateFromTypeface(long nativeInstance, int style) {
+    return TypefaceNatives.nativeCreateFromTypeface(nativeInstance, style);
+  }
+
+  @Implementation(minSdk = O)
+  protected static long nativeCreateFromTypefaceWithExactStyle(
+      long nativeInstance, int weight, boolean italic) {
+    return TypefaceNatives.nativeCreateFromTypefaceWithExactStyle(nativeInstance, weight, italic);
+  }
+
+  @Implementation(minSdk = O)
+  protected static long nativeCreateFromTypefaceWithVariation(
+      long nativeInstance, List<FontVariationAxis> axes) {
+    return TypefaceNatives.nativeCreateFromTypefaceWithVariation(nativeInstance, axes);
+  }
+
+  @Implementation(minSdk = LOLLIPOP)
+  protected static long nativeCreateWeightAlias(long nativeInstance, int weight) {
+    return TypefaceNatives.nativeCreateWeightAlias(nativeInstance, weight);
+  }
+
+  @Implementation(minSdk = O, maxSdk = R)
+  protected static long nativeCreateFromArray(long[] familyArray, int weight, int italic) {
+    return TypefaceNatives.nativeCreateFromArray(familyArray, 0, weight, italic);
+  }
+
+  @Implementation(minSdk = S)
+  protected static long nativeCreateFromArray(
+      long[] familyArray, long fallbackTypeface, int weight, int italic) {
+    return TypefaceNatives.nativeCreateFromArray(familyArray, fallbackTypeface, weight, italic);
+  }
+
+  @Implementation(minSdk = O)
+  protected static int[] nativeGetSupportedAxes(long nativeInstance) {
+    return TypefaceNatives.nativeGetSupportedAxes(nativeInstance);
+  }
+
+  @Implementation(minSdk = LOLLIPOP)
+  protected static void nativeSetDefault(long nativePtr) {
+    TypefaceNatives.nativeSetDefault(nativePtr);
+  }
+
+  @Implementation(minSdk = LOLLIPOP)
+  protected static int nativeGetStyle(long nativePtr) {
+    return TypefaceNatives.nativeGetStyle(nativePtr);
+  }
+
+  @Implementation(minSdk = O)
+  protected static int nativeGetWeight(long nativePtr) {
+    return TypefaceNatives.nativeGetWeight(nativePtr);
+  }
+
+  @Implementation(minSdk = P)
+  protected static long nativeGetReleaseFunc() {
+    DefaultNativeRuntimeLoader.injectAndLoad();
+    return TypefaceNatives.nativeGetReleaseFunc();
+  }
+
+  @Implementation(minSdk = S, maxSdk = TIRAMISU)
+  protected static int nativeGetFamilySize(long nativePtr) {
+    return TypefaceNatives.nativeGetFamilySize(nativePtr);
+  }
+
+  @Implementation(minSdk = S, maxSdk = TIRAMISU)
+  protected static long nativeGetFamily(long nativePtr, int index) {
+    return TypefaceNatives.nativeGetFamily(nativePtr, index);
+  }
+
+  @Implementation(minSdk = Q)
+  protected static void nativeRegisterGenericFamily(String str, long nativePtr) {
+    TypefaceNatives.nativeRegisterGenericFamily(str, nativePtr);
+  }
+
+  @Implementation(minSdk = S, maxSdk = TIRAMISU)
+  protected static int nativeWriteTypefaces(ByteBuffer buffer, long[] nativePtrs) {
+    return TypefaceNatives.nativeWriteTypefaces(buffer, nativePtrs);
+  }
+
+  @Implementation(minSdk = 10000)
+  protected static int nativeWriteTypefaces(ByteBuffer buffer, int position, long[] nativePtrs) {
+    return nativeWriteTypefaces(buffer, nativePtrs);
+  }
+
+  @Implementation(minSdk = S, maxSdk = TIRAMISU)
+  protected static long[] nativeReadTypefaces(ByteBuffer buffer) {
+    return TypefaceNatives.nativeReadTypefaces(buffer);
+  }
+
+  @Implementation(minSdk = 10000)
+  protected static long[] nativeReadTypefaces(ByteBuffer buffer, int position) {
+    return nativeReadTypefaces(buffer);
+  }
+
+  @Implementation(minSdk = S)
+  protected static void nativeForceSetStaticFinalField(String fieldName, Typeface typeface) {
+    TypefaceNatives.nativeForceSetStaticFinalField(fieldName, typeface);
+  }
+
+  @Implementation(minSdk = S)
+  protected static void nativeAddFontCollections(long nativePtr) {
+    TypefaceNatives.nativeAddFontCollections(nativePtr);
+  }
+
+  static void ensureInitialized() {
+    try {
+      // Forces static initialization. This should be called before any native code that calls
+      // Typeface::resolveDefault.
+      Class.forName("android.graphics.Typeface");
+    } catch (ClassNotFoundException e) {
+      throw new LinkageError("Unable to load Typeface", e);
+    }
+  }
+
+  @Override
+  public FontDesc getFontDescription() {
+    throw new UnsupportedOperationException(
+        "Legacy ShadowTypeface description APIs are not supported");
+  }
+
+  /**
+   * Shadow for {@link Typeface.Builder}. It is empty to avoid using the legacy {@link
+   * Typeface.Builder} shadow.
+   */
+  @Implements(
+      value = Typeface.Builder.class,
+      minSdk = P,
+      shadowPicker = ShadowNativeTypefaceBuilder.Picker.class,
+      isInAndroidSdk = false)
+  public static class ShadowNativeTypefaceBuilder {
+    /** Shadow picker for {@link Typeface.Builder}. */
+    public static final class Picker extends GraphicsShadowPicker<Object> {
+      public Picker() {
+        super(ShadowLegacyTypeface.ShadowBuilder.class, ShadowNativeTypefaceBuilder.class);
+      }
+    }
+  }
+
+  @ForType(Typeface.class)
+  interface TypefaceReflector {
+    @CanIgnoreReturnValue
+    @Static
+    @Direct
+    FontConfig.Alias[] buildSystemFallback(
+        String xmlPath,
+        String fontDir,
+        ArrayMap<String, Typeface> fontMap,
+        ArrayMap<String, FontFamily[]> fallbackMap);
+  }
+
+  @ForType(className = "android.text.FontConfig$Family")
+  interface FontConfigFamilyReflector {
+    String getLanguage();
+
+    int getVariant();
+
+    FontConfig.Font[] getFonts();
+
+    String getName();
+  }
+
+  @ForType(className = "android.text.FontConfig$Font")
+  interface FontConfigFontReflector {
+    String getFontName();
+  }
+}
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeVectorDrawable.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeVectorDrawable.java
new file mode 100644
index 0000000..88a4a76
--- /dev/null
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeVectorDrawable.java
@@ -0,0 +1,343 @@
+package org.robolectric.shadows;
+
+import static android.os.Build.VERSION_CODES.O;
+import static android.os.Build.VERSION_CODES.Q;
+
+import android.graphics.Rect;
+import android.graphics.drawable.VectorDrawable;
+import org.robolectric.annotation.Implementation;
+import org.robolectric.annotation.Implements;
+import org.robolectric.nativeruntime.DefaultNativeRuntimeLoader;
+import org.robolectric.nativeruntime.VectorDrawableNatives;
+import org.robolectric.shadows.ShadowNativeVectorDrawable.Picker;
+
+/** Shadow for {@link VectorDrawable} that is backed by native code */
+@Implements(
+    value = VectorDrawable.class,
+    minSdk = O,
+    shadowPicker = Picker.class,
+    isInAndroidSdk = false)
+public class ShadowNativeVectorDrawable extends ShadowDrawable {
+
+  @Implementation(minSdk = O)
+  protected static int nDraw(
+      long rendererPtr,
+      long canvasWrapperPtr,
+      long colorFilterPtr,
+      Rect bounds,
+      boolean needsMirroring,
+      boolean canReuseCache) {
+    return VectorDrawableNatives.nDraw(
+        rendererPtr, canvasWrapperPtr, colorFilterPtr, bounds, needsMirroring, canReuseCache);
+  }
+
+  @Implementation(minSdk = O)
+  protected static boolean nGetFullPathProperties(long pathPtr, byte[] properties, int length) {
+    return VectorDrawableNatives.nGetFullPathProperties(pathPtr, properties, length);
+  }
+
+  @Implementation(minSdk = O)
+  protected static void nSetName(long nodePtr, String name) {
+    VectorDrawableNatives.nSetName(nodePtr, name);
+  }
+
+  @Implementation(minSdk = O)
+  protected static boolean nGetGroupProperties(long groupPtr, float[] properties, int length) {
+    return VectorDrawableNatives.nGetGroupProperties(groupPtr, properties, length);
+  }
+
+  @Implementation(minSdk = O)
+  protected static void nSetPathString(long pathPtr, String pathString, int length) {
+    VectorDrawableNatives.nSetPathString(pathPtr, pathString, length);
+  }
+
+  @Implementation(minSdk = O)
+  protected static long nCreateTree(long rootGroupPtr) {
+    return VectorDrawableNatives.nCreateTree(rootGroupPtr);
+  }
+
+  @Implementation(minSdk = O)
+  protected static long nCreateTreeFromCopy(long treeToCopy, long rootGroupPtr) {
+    return VectorDrawableNatives.nCreateTreeFromCopy(treeToCopy, rootGroupPtr);
+  }
+
+  @Implementation(minSdk = O)
+  protected static void nSetRendererViewportSize(
+      long rendererPtr, float viewportWidth, float viewportHeight) {
+    VectorDrawableNatives.nSetRendererViewportSize(rendererPtr, viewportWidth, viewportHeight);
+  }
+
+  @Implementation(minSdk = O)
+  protected static boolean nSetRootAlpha(long rendererPtr, float alpha) {
+    return VectorDrawableNatives.nSetRootAlpha(rendererPtr, alpha);
+  }
+
+  @Implementation(minSdk = O)
+  protected static float nGetRootAlpha(long rendererPtr) {
+    return VectorDrawableNatives.nGetRootAlpha(rendererPtr);
+  }
+
+  @Implementation(minSdk = Q)
+  protected static void nSetAntiAlias(long rendererPtr, boolean aa) {
+    VectorDrawableNatives.nSetAntiAlias(rendererPtr, aa);
+  }
+
+  @Implementation(minSdk = O)
+  protected static void nSetAllowCaching(long rendererPtr, boolean allowCaching) {
+    VectorDrawableNatives.nSetAllowCaching(rendererPtr, allowCaching);
+  }
+
+  @Implementation(minSdk = O)
+  protected static long nCreateFullPath() {
+    return VectorDrawableNatives.nCreateFullPath();
+  }
+
+  @Implementation(minSdk = O)
+  protected static long nCreateFullPath(long nativeFullPathPtr) {
+    return VectorDrawableNatives.nCreateFullPath(nativeFullPathPtr);
+  }
+
+  @Implementation(minSdk = O)
+  protected static void nUpdateFullPathProperties(
+      long pathPtr,
+      float strokeWidth,
+      int strokeColor,
+      float strokeAlpha,
+      int fillColor,
+      float fillAlpha,
+      float trimPathStart,
+      float trimPathEnd,
+      float trimPathOffset,
+      float strokeMiterLimit,
+      int strokeLineCap,
+      int strokeLineJoin,
+      int fillType) {
+    VectorDrawableNatives.nUpdateFullPathProperties(
+        pathPtr,
+        strokeWidth,
+        strokeColor,
+        strokeAlpha,
+        fillColor,
+        fillAlpha,
+        trimPathStart,
+        trimPathEnd,
+        trimPathOffset,
+        strokeMiterLimit,
+        strokeLineCap,
+        strokeLineJoin,
+        fillType);
+  }
+
+  @Implementation(minSdk = O)
+  protected static void nUpdateFullPathFillGradient(long pathPtr, long fillGradientPtr) {
+    VectorDrawableNatives.nUpdateFullPathFillGradient(pathPtr, fillGradientPtr);
+  }
+
+  @Implementation(minSdk = O)
+  protected static void nUpdateFullPathStrokeGradient(long pathPtr, long strokeGradientPtr) {
+    VectorDrawableNatives.nUpdateFullPathStrokeGradient(pathPtr, strokeGradientPtr);
+  }
+
+  @Implementation(minSdk = O)
+  protected static long nCreateClipPath() {
+    return VectorDrawableNatives.nCreateClipPath();
+  }
+
+  @Implementation(minSdk = O)
+  protected static long nCreateClipPath(long clipPathPtr) {
+    return VectorDrawableNatives.nCreateClipPath(clipPathPtr);
+  }
+
+  @Implementation(minSdk = O)
+  protected static long nCreateGroup() {
+    DefaultNativeRuntimeLoader.injectAndLoad();
+    return VectorDrawableNatives.nCreateGroup();
+  }
+
+  @Implementation(minSdk = O)
+  protected static long nCreateGroup(long groupPtr) {
+    DefaultNativeRuntimeLoader.injectAndLoad();
+    return VectorDrawableNatives.nCreateGroup(groupPtr);
+  }
+
+  @Implementation(minSdk = O)
+  protected static void nUpdateGroupProperties(
+      long groupPtr,
+      float rotate,
+      float pivotX,
+      float pivotY,
+      float scaleX,
+      float scaleY,
+      float translateX,
+      float translateY) {
+    VectorDrawableNatives.nUpdateGroupProperties(
+        groupPtr, rotate, pivotX, pivotY, scaleX, scaleY, translateX, translateY);
+  }
+
+  @Implementation(minSdk = O)
+  protected static void nAddChild(long groupPtr, long nodePtr) {
+    VectorDrawableNatives.nAddChild(groupPtr, nodePtr);
+  }
+
+  @Implementation(minSdk = O)
+  protected static float nGetRotation(long groupPtr) {
+    return VectorDrawableNatives.nGetRotation(groupPtr);
+  }
+
+  @Implementation(minSdk = O)
+  protected static void nSetRotation(long groupPtr, float rotation) {
+    VectorDrawableNatives.nSetRotation(groupPtr, rotation);
+  }
+
+  @Implementation(minSdk = O)
+  protected static float nGetPivotX(long groupPtr) {
+    return VectorDrawableNatives.nGetPivotX(groupPtr);
+  }
+
+  @Implementation(minSdk = O)
+  protected static void nSetPivotX(long groupPtr, float pivotX) {
+    VectorDrawableNatives.nSetPivotX(groupPtr, pivotX);
+  }
+
+  @Implementation(minSdk = O)
+  protected static float nGetPivotY(long groupPtr) {
+    return VectorDrawableNatives.nGetPivotY(groupPtr);
+  }
+
+  @Implementation(minSdk = O)
+  protected static void nSetPivotY(long groupPtr, float pivotY) {
+    VectorDrawableNatives.nSetPivotY(groupPtr, pivotY);
+  }
+
+  @Implementation(minSdk = O)
+  protected static float nGetScaleX(long groupPtr) {
+    return VectorDrawableNatives.nGetScaleX(groupPtr);
+  }
+
+  @Implementation(minSdk = O)
+  protected static void nSetScaleX(long groupPtr, float scaleX) {
+    VectorDrawableNatives.nSetScaleX(groupPtr, scaleX);
+  }
+
+  @Implementation(minSdk = O)
+  protected static float nGetScaleY(long groupPtr) {
+    return VectorDrawableNatives.nGetScaleY(groupPtr);
+  }
+
+  @Implementation(minSdk = O)
+  protected static void nSetScaleY(long groupPtr, float scaleY) {
+    VectorDrawableNatives.nSetScaleY(groupPtr, scaleY);
+  }
+
+  @Implementation(minSdk = O)
+  protected static float nGetTranslateX(long groupPtr) {
+    return VectorDrawableNatives.nGetTranslateX(groupPtr);
+  }
+
+  @Implementation(minSdk = O)
+  protected static void nSetTranslateX(long groupPtr, float translateX) {
+    VectorDrawableNatives.nSetTranslateX(groupPtr, translateX);
+  }
+
+  @Implementation(minSdk = O)
+  protected static float nGetTranslateY(long groupPtr) {
+    return VectorDrawableNatives.nGetTranslateY(groupPtr);
+  }
+
+  @Implementation(minSdk = O)
+  protected static void nSetTranslateY(long groupPtr, float translateY) {
+    VectorDrawableNatives.nSetTranslateY(groupPtr, translateY);
+  }
+
+  @Implementation(minSdk = O)
+  protected static void nSetPathData(long pathPtr, long pathDataPtr) {
+    VectorDrawableNatives.nSetPathData(pathPtr, pathDataPtr);
+  }
+
+  @Implementation(minSdk = O)
+  protected static float nGetStrokeWidth(long pathPtr) {
+    return VectorDrawableNatives.nGetStrokeWidth(pathPtr);
+  }
+
+  @Implementation(minSdk = O)
+  protected static void nSetStrokeWidth(long pathPtr, float width) {
+    VectorDrawableNatives.nSetStrokeWidth(pathPtr, width);
+  }
+
+  @Implementation(minSdk = O)
+  protected static int nGetStrokeColor(long pathPtr) {
+    return VectorDrawableNatives.nGetStrokeColor(pathPtr);
+  }
+
+  @Implementation(minSdk = O)
+  protected static void nSetStrokeColor(long pathPtr, int strokeColor) {
+    VectorDrawableNatives.nSetStrokeColor(pathPtr, strokeColor);
+  }
+
+  @Implementation(minSdk = O)
+  protected static float nGetStrokeAlpha(long pathPtr) {
+    return VectorDrawableNatives.nGetStrokeAlpha(pathPtr);
+  }
+
+  @Implementation(minSdk = O)
+  protected static void nSetStrokeAlpha(long pathPtr, float alpha) {
+    VectorDrawableNatives.nSetStrokeAlpha(pathPtr, alpha);
+  }
+
+  @Implementation(minSdk = O)
+  protected static int nGetFillColor(long pathPtr) {
+    return VectorDrawableNatives.nGetFillColor(pathPtr);
+  }
+
+  @Implementation(minSdk = O)
+  protected static void nSetFillColor(long pathPtr, int fillColor) {
+    VectorDrawableNatives.nSetFillColor(pathPtr, fillColor);
+  }
+
+  @Implementation(minSdk = O)
+  protected static float nGetFillAlpha(long pathPtr) {
+    return VectorDrawableNatives.nGetFillAlpha(pathPtr);
+  }
+
+  @Implementation(minSdk = O)
+  protected static void nSetFillAlpha(long pathPtr, float fillAlpha) {
+    VectorDrawableNatives.nSetFillAlpha(pathPtr, fillAlpha);
+  }
+
+  @Implementation(minSdk = O)
+  protected static float nGetTrimPathStart(long pathPtr) {
+    return VectorDrawableNatives.nGetTrimPathStart(pathPtr);
+  }
+
+  @Implementation(minSdk = O)
+  protected static void nSetTrimPathStart(long pathPtr, float trimPathStart) {
+    VectorDrawableNatives.nSetTrimPathStart(pathPtr, trimPathStart);
+  }
+
+  @Implementation(minSdk = O)
+  protected static float nGetTrimPathEnd(long pathPtr) {
+    return VectorDrawableNatives.nGetTrimPathEnd(pathPtr);
+  }
+
+  @Implementation(minSdk = O)
+  protected static void nSetTrimPathEnd(long pathPtr, float trimPathEnd) {
+    VectorDrawableNatives.nSetTrimPathEnd(pathPtr, trimPathEnd);
+  }
+
+  @Implementation(minSdk = O)
+  protected static float nGetTrimPathOffset(long pathPtr) {
+    return VectorDrawableNatives.nGetTrimPathOffset(pathPtr);
+  }
+
+  @Implementation(minSdk = O)
+  protected static void nSetTrimPathOffset(long pathPtr, float trimPathOffset) {
+    VectorDrawableNatives.nSetTrimPathOffset(pathPtr, trimPathOffset);
+  }
+
+  /** Shadow picker for {@link VectorDrawable}. */
+  public static final class Picker extends GraphicsShadowPicker<Object> {
+    public Picker() {
+      super(ShadowVectorDrawable.class, ShadowNativeVectorDrawable.class);
+    }
+  }
+}
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeVirtualRefBasePtr.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeVirtualRefBasePtr.java
new file mode 100644
index 0000000..8343912
--- /dev/null
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeVirtualRefBasePtr.java
@@ -0,0 +1,35 @@
+package org.robolectric.shadows;
+
+import static android.os.Build.VERSION_CODES.O;
+
+import com.android.internal.util.VirtualRefBasePtr;
+import org.robolectric.annotation.Implementation;
+import org.robolectric.annotation.Implements;
+import org.robolectric.nativeruntime.VirtualRefBasePtrNatives;
+import org.robolectric.shadows.ShadowNativeVirtualRefBasePtr.Picker;
+
+/** Shadow for {@link VirtualRefBasePtr} that is backed by native code */
+@Implements(
+    value = VirtualRefBasePtr.class,
+    minSdk = O,
+    shadowPicker = Picker.class,
+    isInAndroidSdk = false)
+public class ShadowNativeVirtualRefBasePtr {
+
+  @Implementation(minSdk = O)
+  protected static void nIncStrong(long ptr) {
+    VirtualRefBasePtrNatives.nIncStrong(ptr);
+  }
+
+  @Implementation(minSdk = O)
+  protected static void nDecStrong(long ptr) {
+    VirtualRefBasePtrNatives.nDecStrong(ptr);
+  }
+
+  /** Shadow picker for {@link VirtualRefBasePtr}. */
+  public static final class Picker extends GraphicsShadowPicker<Object> {
+    public Picker() {
+      super(ShadowVirtualRefBasePtr.class, ShadowNativeVirtualRefBasePtr.class);
+    }
+  }
+}
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNetworkCapabilities.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNetworkCapabilities.java
index 39e3d9b..4f03aa3 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNetworkCapabilities.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNetworkCapabilities.java
@@ -19,7 +19,7 @@
 import org.robolectric.util.reflector.ForType;
 
 /** Robolectic provides overrides for fetching and updating transport. */
-@Implements(value = NetworkCapabilities.class, minSdk = LOLLIPOP)
+@Implements(value = NetworkCapabilities.class, minSdk = LOLLIPOP, looseSignatures = true)
 public class ShadowNetworkCapabilities {
 
   @RealObject protected NetworkCapabilities realNetworkCapabilities;
@@ -90,10 +90,12 @@
 
   /** Sets the LinkDownstreamBandwidthKbps of the NetworkCapabilities. */
   @HiddenApi
-  @Implementation(minSdk = Q)
-  public NetworkCapabilities setLinkDownstreamBandwidthKbps(int kbps) {
+  @Implementation
+  public Object setLinkDownstreamBandwidthKbps(Object kbps) {
+    // Loose signatures is necessary because the return type of setLinkDownstreamBandwidthKbps
+    // changed from void to NetworkCapabilities starting from API 28 (Pie)
     return reflector(NetworkCapabilitiesReflector.class, realNetworkCapabilities)
-        .setLinkDownstreamBandwidthKbps(kbps);
+        .setLinkDownstreamBandwidthKbps((int) kbps);
   }
 
   @ForType(NetworkCapabilities.class)
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNoopNativeAllocationRegistry.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNoopNativeAllocationRegistry.java
new file mode 100644
index 0000000..7310b5c
--- /dev/null
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNoopNativeAllocationRegistry.java
@@ -0,0 +1,26 @@
+package org.robolectric.shadows;
+
+import static android.os.Build.VERSION_CODES.N;
+
+import libcore.util.NativeAllocationRegistry;
+import org.robolectric.annotation.Implementation;
+import org.robolectric.annotation.Implements;
+
+/** Shadow for {@link NativeAllocationRegistry} that is a no-op. */
+@Implements(
+    value = NativeAllocationRegistry.class,
+    minSdk = N,
+    isInAndroidSdk = false,
+    looseSignatures = true)
+public class ShadowNoopNativeAllocationRegistry {
+
+  @Implementation
+  protected Runnable registerNativeAllocation(Object referent, Object allocator) {
+    return () -> {};
+  }
+
+  @Implementation
+  protected Runnable registerNativeAllocation(Object referent, long nativePtr) {
+    return () -> {};
+  }
+}
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNumberPicker.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNumberPicker.java
index 8cf21f9..3b9889f 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNumberPicker.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNumberPicker.java
@@ -12,61 +12,25 @@
 @Implements(value = NumberPicker.class)
 public class ShadowNumberPicker extends ShadowLinearLayout {
   @RealObject private NumberPicker realNumberPicker;
-  private int value;
-  private int minValue;
-  private int maxValue;
-  private boolean wrapSelectorWheel;
   private String[] displayedValues;
   private NumberPicker.OnValueChangeListener onValueChangeListener;
 
   @Implementation
-  protected void setValue(int value) {
-    this.value = value;
-  }
-
-  @Implementation
-  protected int getValue() {
-    return value;
-  }
-
-  @Implementation
   protected void setDisplayedValues(String[] displayedValues) {
-    this.displayedValues = displayedValues;
+    if (ShadowView.useRealGraphics()) {
+      reflector(NumberPickerReflector.class, realNumberPicker).setDisplayedValues(displayedValues);
+    } else {
+      this.displayedValues = displayedValues;
+    }
   }
 
   @Implementation
   protected String[] getDisplayedValues() {
-    return displayedValues;
-  }
-
-  @Implementation
-  protected void setMinValue(int minValue) {
-    this.minValue = minValue;
-  }
-
-  @Implementation
-  protected void setMaxValue(int maxValue) {
-    this.maxValue = maxValue;
-  }
-
-  @Implementation
-  protected int getMinValue() {
-    return this.minValue;
-  }
-
-  @Implementation
-  protected int getMaxValue() {
-    return this.maxValue;
-  }
-
-  @Implementation
-  protected void setWrapSelectorWheel(boolean wrapSelectorWheel) {
-    this.wrapSelectorWheel = wrapSelectorWheel;
-  }
-
-  @Implementation
-  protected boolean getWrapSelectorWheel() {
-    return wrapSelectorWheel;
+    if (ShadowView.useRealGraphics()) {
+      return reflector(NumberPickerReflector.class, realNumberPicker).getDisplayedValues();
+    } else {
+      return displayedValues;
+    }
   }
 
   @Implementation
@@ -84,5 +48,11 @@
 
     @Direct
     void setOnValueChangedListener(NumberPicker.OnValueChangeListener listener);
+
+    @Direct
+    void setDisplayedValues(String[] displayedValues);
+
+    @Direct
+    String[] getDisplayedValues();
   }
 }
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowOutline.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowOutline.java
deleted file mode 100644
index d094668..0000000
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowOutline.java
+++ /dev/null
@@ -1,15 +0,0 @@
-package org.robolectric.shadows;
-
-import static android.os.Build.VERSION_CODES.LOLLIPOP;
-
-import android.graphics.Outline;
-import android.graphics.Path;
-import org.robolectric.annotation.Implementation;
-import org.robolectric.annotation.Implements;
-
-@Implements(value = Outline.class, minSdk = LOLLIPOP)
-public class ShadowOutline {
-
-  @Implementation
-  protected void setConvexPath(Path convexPath) {}
-}
\ No newline at end of file
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowPath.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowPath.java
index 889736f..de99c01 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowPath.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowPath.java
@@ -1,163 +1,28 @@
 package org.robolectric.shadows;
 
-import static android.os.Build.VERSION_CODES.JELLY_BEAN;
-import static android.os.Build.VERSION_CODES.KITKAT;
-import static android.os.Build.VERSION_CODES.LOLLIPOP;
-import static org.robolectric.shadow.api.Shadow.extract;
-import static org.robolectric.shadows.ShadowPath.Point.Type.LINE_TO;
-import static org.robolectric.shadows.ShadowPath.Point.Type.MOVE_TO;
 
-import android.graphics.Matrix;
 import android.graphics.Path;
-import android.graphics.Path.Direction;
 import android.graphics.RectF;
-import android.util.Log;
-import java.awt.geom.AffineTransform;
-import java.awt.geom.Arc2D;
-import java.awt.geom.Area;
-import java.awt.geom.Ellipse2D;
-import java.awt.geom.GeneralPath;
-import java.awt.geom.Path2D;
-import java.awt.geom.PathIterator;
-import java.awt.geom.Point2D;
-import java.awt.geom.Rectangle2D;
-import java.awt.geom.RoundRectangle2D;
-import java.util.ArrayList;
 import java.util.List;
-import org.robolectric.annotation.Implementation;
 import org.robolectric.annotation.Implements;
-import org.robolectric.annotation.RealObject;
+import org.robolectric.shadows.ShadowPath.Picker;
 
-/**
- * The shadow only supports straight-line paths.
- */
+/** Base class for {@link ShadowPath} classes. */
 @SuppressWarnings({"UnusedDeclaration"})
-@Implements(Path.class)
-public class ShadowPath {
-  private static final String TAG = ShadowPath.class.getSimpleName();
-  private static final float EPSILON = 1e-4f;
-
-  @RealObject private Path realObject;
-
-  private List<Point> points = new ArrayList<>();
-
-  private float mLastX = 0;
-  private float mLastY = 0;
-  private Path2D mPath = new Path2D.Double();
-  private boolean mCachedIsEmpty = true;
-  private Path.FillType mFillType = Path.FillType.WINDING;
-  protected boolean isSimplePath;
-
-  @Implementation
-  protected void __constructor__(Path path) {
-    ShadowPath shadowPath = extract(path);
-    points = new ArrayList<>(shadowPath.getPoints());
-    mPath.append(shadowPath.mPath, /*connect=*/ false);
-    mFillType = shadowPath.getFillType();
-  }
-
-  Path2D getJavaShape() {
-    return mPath;
-  }
-
-  @Implementation
-  protected void moveTo(float x, float y) {
-    mPath.moveTo(mLastX = x, mLastY = y);
-
-    // Legacy recording behavior
-    Point p = new Point(x, y, MOVE_TO);
-    points.add(p);
-  }
-
-  @Implementation
-  protected void lineTo(float x, float y) {
-    if (!hasPoints()) {
-      mPath.moveTo(mLastX = 0, mLastY = 0);
-    }
-    mPath.lineTo(mLastX = x, mLastY = y);
-
-    // Legacy recording behavior
-    Point point = new Point(x, y, LINE_TO);
-    points.add(point);
-  }
-
-  @Implementation
-  protected void quadTo(float x1, float y1, float x2, float y2) {
-    isSimplePath = false;
-    if (!hasPoints()) {
-      moveTo(0, 0);
-    }
-    mPath.quadTo(x1, y1, mLastX = x2, mLastY = y2);
-  }
-
-  @Implementation
-  protected void cubicTo(float x1, float y1, float x2, float y2, float x3, float y3) {
-    if (!hasPoints()) {
-      mPath.moveTo(0, 0);
-    }
-    mPath.curveTo(x1, y1, x2, y2, mLastX = x3, mLastY = y3);
-  }
-
-  private boolean hasPoints() {
-    return !mPath.getPathIterator(null).isDone();
-  }
-
-  @Implementation
-  protected void reset() {
-    mPath.reset();
-    mLastX = 0;
-    mLastY = 0;
-
-    // Legacy recording behavior
-    points.clear();
-  }
-
-  @Implementation(minSdk = LOLLIPOP)
-  protected float[] approximate(float acceptableError) {
-    PathIterator iterator = mPath.getPathIterator(null, acceptableError);
-
-    float segment[] = new float[6];
-    float totalLength = 0;
-    ArrayList<Point2D.Float> points = new ArrayList<Point2D.Float>();
-    Point2D.Float previousPoint = null;
-    while (!iterator.isDone()) {
-      int type = iterator.currentSegment(segment);
-      Point2D.Float currentPoint = new Point2D.Float(segment[0], segment[1]);
-      // MoveTo shouldn't affect the length
-      if (previousPoint != null && type != PathIterator.SEG_MOVETO) {
-        totalLength += (float) currentPoint.distance(previousPoint);
-      }
-      previousPoint = currentPoint;
-      points.add(currentPoint);
-      iterator.next();
-    }
-
-    int nPoints = points.size();
-    float[] result = new float[nPoints * 3];
-    previousPoint = null;
-    // Distance that we've covered so far. Used to calculate the fraction of the path that
-    // we've covered up to this point.
-    float walkedDistance = .0f;
-    for (int i = 0; i < nPoints; i++) {
-      Point2D.Float point = points.get(i);
-      float distance = previousPoint != null ? (float) previousPoint.distance(point) : .0f;
-      walkedDistance += distance;
-      result[i * 3] = walkedDistance / totalLength;
-      result[i * 3 + 1] = point.x;
-      result[i * 3 + 2] = point.y;
-
-      previousPoint = point;
-    }
-
-    return result;
-  }
+@Implements(value = Path.class, shadowPicker = Picker.class)
+public abstract class ShadowPath {
 
   /**
    * @return all the points that have been added to the {@code Path}
    */
-  public List<Point> getPoints() {
-    return points;
-  }
+  public abstract List<Point> getPoints();
+
+  /**
+   * Fills the given {@link RectF} with the path bounds.
+   *
+   * @param bounds the RectF to be filled.
+   */
+  public abstract void fillBounds(RectF bounds);
 
   public static class Point {
     private final float x;
@@ -215,400 +80,10 @@
     }
   }
 
-  @Implementation
-  protected void rewind() {
-    // call out to reset since there's nothing to optimize in
-    // terms of data structs.
-    reset();
-  }
-
-  @Implementation
-  protected void set(Path src) {
-    mPath.reset();
-
-    ShadowPath shadowSrc = extract(src);
-    setFillType(shadowSrc.mFillType);
-    mPath.append(shadowSrc.mPath, false /*connect*/);
-  }
-
-  @Implementation(minSdk = KITKAT)
-  protected boolean op(Path path1, Path path2, Path.Op op) {
-    Log.w(TAG, "android.graphics.Path#op() not supported yet.");
-    return false;
-  }
-
-  @Implementation(minSdk = LOLLIPOP)
-  protected boolean isConvex() {
-    Log.w(TAG, "android.graphics.Path#isConvex() not supported yet.");
-    return true;
-  }
-
-  @Implementation
-  protected Path.FillType getFillType() {
-    return mFillType;
-  }
-
-  @Implementation
-  protected void setFillType(Path.FillType fillType) {
-    mFillType = fillType;
-    mPath.setWindingRule(getWindingRule(fillType));
-  }
-
-  /**
-   * Returns the Java2D winding rules matching a given Android {@link FillType}.
-   *
-   * @param type the android fill type
-   * @return the matching java2d winding rule.
-   */
-  private static int getWindingRule(Path.FillType type) {
-    switch (type) {
-      case WINDING:
-      case INVERSE_WINDING:
-        return GeneralPath.WIND_NON_ZERO;
-      case EVEN_ODD:
-      case INVERSE_EVEN_ODD:
-        return GeneralPath.WIND_EVEN_ODD;
-
-      default:
-        assert false;
-        return GeneralPath.WIND_NON_ZERO;
+  /** Shadow picker for {@link Path}. */
+  public static final class Picker extends GraphicsShadowPicker<Object> {
+    public Picker() {
+      super(ShadowLegacyPath.class, ShadowNativePath.class);
     }
   }
-
-  @Implementation
-  protected boolean isInverseFillType() {
-    throw new UnsupportedOperationException("isInverseFillType");
-  }
-
-  @Implementation
-  protected void toggleInverseFillType() {
-    throw new UnsupportedOperationException("toggleInverseFillType");
-  }
-
-  @Implementation
-  protected boolean isEmpty() {
-    if (!mCachedIsEmpty) {
-      return false;
-    }
-
-    float[] coords = new float[6];
-    mCachedIsEmpty = Boolean.TRUE;
-    for (PathIterator it = mPath.getPathIterator(null); !it.isDone(); it.next()) {
-      // int type = it.currentSegment(coords);
-      // if (type != PathIterator.SEG_MOVETO) {
-      // Once we know that the path is not empty, we do not need to check again unless
-      // Path#reset is called.
-      mCachedIsEmpty = false;
-      return false;
-      // }
-    }
-
-    return true;
-  }
-
-  @Implementation
-  protected boolean isRect(RectF rect) {
-    // create an Area that can test if the path is a rect
-    Area area = new Area(mPath);
-    if (area.isRectangular()) {
-      if (rect != null) {
-        fillBounds(rect);
-      }
-
-      return true;
-    }
-
-    return false;
-  }
-
-  @Implementation
-  protected void computeBounds(RectF bounds, boolean exact) {
-    fillBounds(bounds);
-  }
-
-  @Implementation
-  protected void incReserve(int extraPtCount) {
-    throw new UnsupportedOperationException("incReserve");
-  }
-
-  @Implementation
-  protected void rMoveTo(float dx, float dy) {
-    dx += mLastX;
-    dy += mLastY;
-    mPath.moveTo(mLastX = dx, mLastY = dy);
-  }
-
-  @Implementation
-  protected void rLineTo(float dx, float dy) {
-    if (!hasPoints()) {
-      mPath.moveTo(mLastX = 0, mLastY = 0);
-    }
-
-    if (Math.abs(dx) < EPSILON && Math.abs(dy) < EPSILON) {
-      // The delta is so small that this shouldn't generate a line
-      return;
-    }
-
-    dx += mLastX;
-    dy += mLastY;
-    mPath.lineTo(mLastX = dx, mLastY = dy);
-  }
-
-  @Implementation
-  protected void rQuadTo(float dx1, float dy1, float dx2, float dy2) {
-    if (!hasPoints()) {
-      mPath.moveTo(mLastX = 0, mLastY = 0);
-    }
-    dx1 += mLastX;
-    dy1 += mLastY;
-    dx2 += mLastX;
-    dy2 += mLastY;
-    mPath.quadTo(dx1, dy1, mLastX = dx2, mLastY = dy2);
-  }
-
-  @Implementation
-  protected void rCubicTo(float x1, float y1, float x2, float y2, float x3, float y3) {
-    if (!hasPoints()) {
-      mPath.moveTo(mLastX = 0, mLastY = 0);
-    }
-    x1 += mLastX;
-    y1 += mLastY;
-    x2 += mLastX;
-    y2 += mLastY;
-    x3 += mLastX;
-    y3 += mLastY;
-    mPath.curveTo(x1, y1, x2, y2, mLastX = x3, mLastY = y3);
-  }
-
-  @Implementation
-  protected void arcTo(RectF oval, float startAngle, float sweepAngle) {
-    arcTo(oval.left, oval.top, oval.right, oval.bottom, startAngle, sweepAngle, false);
-  }
-
-  @Implementation
-  protected void arcTo(RectF oval, float startAngle, float sweepAngle, boolean forceMoveTo) {
-    arcTo(oval.left, oval.top, oval.right, oval.bottom, startAngle, sweepAngle, forceMoveTo);
-  }
-
-  @Implementation(minSdk = LOLLIPOP)
-  protected void arcTo(
-      float left,
-      float top,
-      float right,
-      float bottom,
-      float startAngle,
-      float sweepAngle,
-      boolean forceMoveTo) {
-    isSimplePath = false;
-    Arc2D arc =
-        new Arc2D.Float(
-            left, top, right - left, bottom - top, -startAngle, -sweepAngle, Arc2D.OPEN);
-    mPath.append(arc, true /*connect*/);
-    if (hasPoints()) {
-      resetLastPointFromPath();
-    }
-  }
-
-  @Implementation
-  protected void close() {
-    if (!hasPoints()) {
-      mPath.moveTo(mLastX = 0, mLastY = 0);
-    }
-    mPath.closePath();
-  }
-
-  @Implementation
-  protected void addRect(RectF rect, Direction dir) {
-    addRect(rect.left, rect.top, rect.right, rect.bottom, dir);
-  }
-
-  @Implementation
-  protected void addRect(float left, float top, float right, float bottom, Path.Direction dir) {
-    moveTo(left, top);
-
-    switch (dir) {
-      case CW:
-        lineTo(right, top);
-        lineTo(right, bottom);
-        lineTo(left, bottom);
-        break;
-      case CCW:
-        lineTo(left, bottom);
-        lineTo(right, bottom);
-        lineTo(right, top);
-        break;
-    }
-
-    close();
-
-    resetLastPointFromPath();
-  }
-
-  @Implementation(minSdk = LOLLIPOP)
-  protected void addOval(float left, float top, float right, float bottom, Path.Direction dir) {
-    mPath.append(new Ellipse2D.Float(left, top, right - left, bottom - top), false);
-  }
-
-  @Implementation
-  protected void addCircle(float x, float y, float radius, Path.Direction dir) {
-    mPath.append(new Ellipse2D.Float(x - radius, y - radius, radius * 2, radius * 2), false);
-  }
-
-  @Implementation(minSdk = LOLLIPOP)
-  protected void addArc(
-      float left, float top, float right, float bottom, float startAngle, float sweepAngle) {
-    mPath.append(
-        new Arc2D.Float(
-            left, top, right - left, bottom - top, -startAngle, -sweepAngle, Arc2D.OPEN),
-        false);
-  }
-
-  @Implementation(minSdk = JELLY_BEAN)
-  protected void addRoundRect(RectF rect, float rx, float ry, Direction dir) {
-    addRoundRect(rect.left, rect.top, rect.right, rect.bottom, rx, ry, dir);
-  }
-
-  @Implementation(minSdk = JELLY_BEAN)
-  protected void addRoundRect(RectF rect, float[] radii, Direction dir) {
-    if (rect == null) {
-      throw new NullPointerException("need rect parameter");
-    }
-    addRoundRect(rect.left, rect.top, rect.right, rect.bottom, radii, dir);
-  }
-
-  @Implementation(minSdk = LOLLIPOP)
-  protected void addRoundRect(
-      float left, float top, float right, float bottom, float rx, float ry, Path.Direction dir) {
-    mPath.append(
-        new RoundRectangle2D.Float(left, top, right - left, bottom - top, rx * 2, ry * 2), false);
-  }
-
-  @Implementation(minSdk = LOLLIPOP)
-  protected void addRoundRect(
-      float left, float top, float right, float bottom, float[] radii, Path.Direction dir) {
-    if (radii.length < 8) {
-      throw new ArrayIndexOutOfBoundsException("radii[] needs 8 values");
-    }
-    isSimplePath = false;
-
-    float[] cornerDimensions = new float[radii.length];
-    for (int i = 0; i < radii.length; i++) {
-      cornerDimensions[i] = 2 * radii[i];
-    }
-    mPath.append(
-        new RoundRectangle(left, top, right - left, bottom - top, cornerDimensions), false);
-  }
-
-  @Implementation
-  protected void addPath(Path src, float dx, float dy) {
-    isSimplePath = false;
-    ShadowPath.addPath(realObject, src, AffineTransform.getTranslateInstance(dx, dy));
-  }
-
-  @Implementation
-  protected void addPath(Path src) {
-    isSimplePath = false;
-    ShadowPath.addPath(realObject, src, null);
-  }
-
-  @Implementation
-  protected void addPath(Path src, Matrix matrix) {
-    if (matrix == null) {
-      return;
-    }
-    ShadowPath shadowSrc = extract(src);
-    if (!shadowSrc.isSimplePath) isSimplePath = false;
-
-    ShadowMatrix shadowMatrix = extract(matrix);
-    ShadowPath.addPath(realObject, src, shadowMatrix.getAffineTransform());
-  }
-
-  private static void addPath(Path destPath, Path srcPath, AffineTransform transform) {
-    if (destPath == null) {
-      return;
-    }
-
-    if (srcPath == null) {
-      return;
-    }
-
-    ShadowPath shadowDestPath = extract(destPath);
-    ShadowPath shadowSrcPath = extract(srcPath);
-    if (transform != null) {
-      shadowDestPath.mPath.append(shadowSrcPath.mPath.getPathIterator(transform), false);
-    } else {
-      shadowDestPath.mPath.append(shadowSrcPath.mPath, false);
-    }
-  }
-
-  @Implementation
-  protected void offset(float dx, float dy, Path dst) {
-    if (dst != null) {
-      dst.set(realObject);
-    } else {
-      dst = realObject;
-    }
-    dst.offset(dx, dy);
-  }
-
-  @Implementation
-  protected void offset(float dx, float dy) {
-    GeneralPath newPath = new GeneralPath();
-
-    PathIterator iterator = mPath.getPathIterator(new AffineTransform(0, 0, dx, 0, 0, dy));
-
-    newPath.append(iterator, false /*connect*/);
-    mPath = newPath;
-  }
-
-  @Implementation
-  protected void setLastPoint(float dx, float dy) {
-    mLastX = dx;
-    mLastY = dy;
-  }
-
-  @Implementation
-  protected void transform(Matrix matrix, Path dst) {
-    ShadowMatrix shadowMatrix = extract(matrix);
-
-    if (shadowMatrix.hasPerspective()) {
-      Log.w(TAG, "android.graphics.Path#transform() only supports affine transformations.");
-    }
-
-    GeneralPath newPath = new GeneralPath();
-
-    PathIterator iterator = mPath.getPathIterator(shadowMatrix.getAffineTransform());
-    newPath.append(iterator, false /*connect*/);
-
-    if (dst != null) {
-      ShadowPath shadowPath = extract(dst);
-      shadowPath.mPath = newPath;
-    } else {
-      mPath = newPath;
-    }
-  }
-
-  @Implementation
-  protected void transform(Matrix matrix) {
-    transform(matrix, null);
-  }
-
-  /**
-   * Fills the given {@link RectF} with the path bounds.
-   *
-   * @param bounds the RectF to be filled.
-   */
-  public void fillBounds(RectF bounds) {
-    Rectangle2D rect = mPath.getBounds2D();
-    bounds.left = (float) rect.getMinX();
-    bounds.right = (float) rect.getMaxX();
-    bounds.top = (float) rect.getMinY();
-    bounds.bottom = (float) rect.getMaxY();
-  }
-
-  private void resetLastPointFromPath() {
-    Point2D last = mPath.getCurrentPoint();
-    mLastX = (float) last.getX();
-    mLastY = (float) last.getY();
-  }
 }
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowPathMeasure.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowPathMeasure.java
index 5bc2b97..4087780 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowPathMeasure.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowPathMeasure.java
@@ -15,7 +15,7 @@
   @Implementation
   protected void __constructor__(Path path, boolean forceClosed) {
     if (path != null) {
-      ShadowPath shadowPath = (ShadowPath) Shadow.extract(path);
+      ShadowLegacyPath shadowPath = Shadow.extract(path);
       mOriginalPathIterator =
           new CachedPathIteratorFactory(shadowPath.getJavaShape().getPathIterator(null));
     }
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowPausedLooper.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowPausedLooper.java
index df4e15b..50f8adf 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowPausedLooper.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowPausedLooper.java
@@ -430,14 +430,10 @@
       setLooperExecutor(this);
       isPaused = true;
       runLatch.countDown();
-      while (true) {
+      while (isPaused) {
         try {
           Runnable runnable = executionQueue.take();
           runnable.run();
-          if (runnable instanceof UnPauseRunnable) {
-            setLooperExecutor(new HandlerExecutor(new Handler(realLooper)));
-            return;
-          }
         } catch (InterruptedException e) {
           // ignore
         }
@@ -448,6 +444,7 @@
   private class UnPauseRunnable extends ControlRunnable {
     @Override
     public void run() {
+      setLooperExecutor(new HandlerExecutor(new Handler(realLooper)));
       isPaused = false;
       runLatch.countDown();
     }
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowPhoneWindow.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowPhoneWindow.java
index 2321329..9411ff1 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowPhoneWindow.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowPhoneWindow.java
@@ -1,11 +1,13 @@
 package org.robolectric.shadows;
 
 import static android.os.Build.VERSION_CODES.M;
+import static android.os.Build.VERSION_CODES.R;
 import static org.robolectric.util.reflector.Reflector.reflector;
 
 import android.graphics.drawable.Drawable;
 import android.view.Gravity;
 import android.view.Window;
+import androidx.annotation.RequiresApi;
 import org.robolectric.annotation.Implementation;
 import org.robolectric.annotation.Implements;
 import org.robolectric.annotation.RealObject;
@@ -17,8 +19,8 @@
 @Implements(className = "com.android.internal.policy.PhoneWindow", isInAndroidSdk = false,
     minSdk = M, looseSignatures = true)
 public class ShadowPhoneWindow extends ShadowWindow {
-  @SuppressWarnings("UnusedDeclaration")
   protected @RealObject Window realWindow;
+  protected boolean decorFitsSystemWindows = true;
 
   @Implementation(minSdk = M)
   public void setTitle(CharSequence title) {
@@ -37,11 +39,29 @@
     return Gravity.CENTER | Gravity.BOTTOM;
   }
 
+  @Implementation(minSdk = R)
+  protected void setDecorFitsSystemWindows(boolean decorFitsSystemWindows) {
+    this.decorFitsSystemWindows = decorFitsSystemWindows;
+    reflector(DirectPhoneWindowReflector.class, realWindow)
+        .setDecorFitsSystemWindows(decorFitsSystemWindows);
+  }
+
+  /**
+   * Returns true with the last value passed to {@link #setDecorFitsSystemWindows(boolean)}, or the
+   * default value (true).
+   */
+  @RequiresApi(R)
+  public boolean getDecorFitsSystemWindows() {
+    return decorFitsSystemWindows;
+  }
+
   @ForType(className = "com.android.internal.policy.PhoneWindow", direct = true)
   interface DirectPhoneWindowReflector {
 
     void setTitle(CharSequence title);
 
     void setBackgroundDrawable(Drawable drawable);
+
+    void setDecorFitsSystemWindows(boolean decorFitsSystemWindows);
   }
 }
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowPosix.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowPosix.java
index 6cf1dbf..c85cf8f 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowPosix.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowPosix.java
@@ -11,10 +11,14 @@
 import org.robolectric.annotation.Implements;
 import org.robolectric.util.ReflectionHelpers;
 
-@Implements(className = "libcore.io.Posix", maxSdk = Build.VERSION_CODES.N_MR1, isInAndroidSdk = false)
+/** Shadow for {@link libcore.io.Posix} */
+@Implements(
+    className = "libcore.io.Posix",
+    maxSdk = Build.VERSION_CODES.N_MR1,
+    isInAndroidSdk = false)
 public class ShadowPosix {
   @Implementation
-  public static void mkdir(String path, int mode) throws ErrnoException {
+  public void mkdir(String path, int mode) throws ErrnoException {
     new File(path).mkdirs();
   }
 
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowPreference.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowPreference.java
index 93f90b2..f3e7355 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowPreference.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowPreference.java
@@ -6,7 +6,6 @@
 import android.preference.PreferenceManager;
 import org.robolectric.annotation.Implements;
 import org.robolectric.annotation.RealObject;
-import org.robolectric.util.reflector.Direct;
 import org.robolectric.util.reflector.ForType;
 
 @Implements(Preference.class)
@@ -23,8 +22,6 @@
 
   @ForType(Preference.class)
   interface PreferenceReflector {
-
-    @Direct
     void onAttachedToHierarchy(PreferenceManager preferenceManager);
   }
 }
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowRecordingCanvas.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowRecordingCanvas.java
index 1048d56..2f91d2d 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowRecordingCanvas.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowRecordingCanvas.java
@@ -7,7 +7,7 @@
 import org.robolectric.annotation.Implements;
 
 @Implements(value = RecordingCanvas.class, isInAndroidSdk = false, minSdk = Q)
-public class ShadowRecordingCanvas extends ShadowCanvas {
+public class ShadowRecordingCanvas extends ShadowLegacyCanvas {
 
   @Implementation
   protected static long nCreateDisplayListCanvas(long node, int width, int height) {
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowScaleGestureDetector.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowScaleGestureDetector.java
index c7f9205..ca5032f 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowScaleGestureDetector.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowScaleGestureDetector.java
@@ -5,27 +5,32 @@
 import android.view.ScaleGestureDetector;
 import org.robolectric.annotation.Implementation;
 import org.robolectric.annotation.Implements;
+import org.robolectric.annotation.ReflectorObject;
+import org.robolectric.util.reflector.Direct;
+import org.robolectric.util.reflector.ForType;
 
 @SuppressWarnings({"UnusedDeclaration"})
 @Implements(ScaleGestureDetector.class)
 public class ShadowScaleGestureDetector {
 
+  @ReflectorObject ScaleGestureDetectorReflector scaleGestureDetectorReflector;
   private MotionEvent onTouchEventMotionEvent;
   private ScaleGestureDetector.OnScaleGestureListener listener;
-  private float scaleFactor = 1;
-  private float focusX;
-  private float focusY;
+  private Float scaleFactor;
+  private Float focusX;
+  private Float focusY;
 
   @Implementation
   protected void __constructor__(
       Context context, ScaleGestureDetector.OnScaleGestureListener listener) {
+    scaleGestureDetectorReflector.__constructor__(context, listener);
     this.listener = listener;
   }
 
   @Implementation
   protected boolean onTouchEvent(MotionEvent event) {
     onTouchEventMotionEvent = event;
-    return true;
+    return scaleGestureDetectorReflector.onTouchEvent(event);
   }
 
   public MotionEvent getOnTouchEventMotionEvent() {
@@ -34,9 +39,9 @@
 
   public void reset() {
     onTouchEventMotionEvent = null;
-    scaleFactor = 1;
-    focusX = 0;
-    focusY = 0;
+    scaleFactor = null;
+    focusX = null;
+    focusY = null;
   }
 
   public ScaleGestureDetector.OnScaleGestureListener getListener() {
@@ -49,7 +54,7 @@
 
   @Implementation
   protected float getScaleFactor() {
-    return scaleFactor;
+    return scaleFactor != null ? scaleFactor : scaleGestureDetectorReflector.getScaleFactor();
   }
 
   public void setFocusXY(float focusX, float focusY) {
@@ -59,11 +64,29 @@
 
   @Implementation
   protected float getFocusX() {
-    return focusX;
+    return focusX != null ? focusX : scaleGestureDetectorReflector.getFocusX();
   }
 
   @Implementation
   protected float getFocusY() {
-    return focusY;
+    return focusY != null ? focusY : scaleGestureDetectorReflector.getFocusY();
+  }
+
+  @ForType(ScaleGestureDetector.class)
+  private interface ScaleGestureDetectorReflector {
+    @Direct
+    void __constructor__(Context context, ScaleGestureDetector.OnScaleGestureListener listener);
+
+    @Direct
+    boolean onTouchEvent(MotionEvent event);
+
+    @Direct
+    float getScaleFactor();
+
+    @Direct
+    float getFocusX();
+
+    @Direct
+    float getFocusY();
   }
 }
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowSearchManager.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowSearchManager.java
index 204dc4c..5c985a5 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowSearchManager.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowSearchManager.java
@@ -1,17 +1,49 @@
 package org.robolectric.shadows;
 
+import static android.os.Build.VERSION_CODES.M;
+import static org.robolectric.util.reflector.Reflector.reflector;
+
+import android.annotation.SystemApi;
 import android.app.SearchManager;
 import android.app.SearchableInfo;
 import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Bundle;
 import org.robolectric.annotation.Implementation;
 import org.robolectric.annotation.Implements;
+import org.robolectric.annotation.RealObject;
+import org.robolectric.util.reflector.Accessor;
+import org.robolectric.util.reflector.ForType;
 
 @Implements(SearchManager.class)
 public class ShadowSearchManager {
 
+  @RealObject private SearchManager searchManager;
+
   @Implementation
   protected SearchableInfo getSearchableInfo(ComponentName componentName) {
     // Prevent Robolectric from calling through
     return null;
   }
+
+  @Implementation(minSdk = M)
+  @SystemApi
+  protected void launchAssist(Bundle bundle) {
+    Intent intent = new Intent(Intent.ACTION_ASSIST);
+    intent.putExtras(bundle);
+    getContext().sendBroadcast(intent);
+  }
+
+  private Context getContext() {
+    return reflector(ReflectorSearchManager.class, searchManager).getContext();
+  }
+
+  /** Reflector interface for {@link SearchManager}'s internals. */
+  @ForType(SearchManager.class)
+  private interface ReflectorSearchManager {
+
+    @Accessor("mContext")
+    Context getContext();
+  }
 }
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowSensorManager.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowSensorManager.java
index 5aa44af..c973fe3 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowSensorManager.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowSensorManager.java
@@ -9,8 +9,10 @@
 import android.hardware.SensorDirectChannel;
 import android.hardware.SensorEvent;
 import android.hardware.SensorEventListener;
+import android.hardware.SensorEventListener2;
 import android.hardware.SensorManager;
 import android.os.Handler;
+import android.os.Looper;
 import android.os.MemoryFile;
 import com.google.common.collect.HashMultimap;
 import com.google.common.collect.ImmutableList;
@@ -146,6 +148,27 @@
     }
   }
 
+  @Implementation(minSdk = KITKAT)
+  protected boolean flush(SensorEventListener listener) {
+    // ShadowSensorManager doesn't queue up any sensor events, so nothing actually needs to be
+    // flushed. Just call onFlushCompleted for each sensor that would have been flushed.
+    new Handler(Looper.getMainLooper())
+        .post(
+            () -> {
+              // Go through each sensor that the listener is registered for, and call
+              // onFlushCompleted on each listener registered for that sensor.
+              for (Sensor sensor : listeners.get(listener)) {
+                for (SensorEventListener registeredListener : getListeners()) {
+                  if ((registeredListener instanceof SensorEventListener2)
+                      && listeners.containsEntry(registeredListener, sensor)) {
+                    ((SensorEventListener2) registeredListener).onFlushCompleted(sensor);
+                  }
+                }
+              }
+            });
+    return listeners.containsKey(listener);
+  }
+
   public SensorEvent createSensorEvent() {
     return ReflectionHelpers.callConstructor(SensorEvent.class);
   }
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowSettings.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowSettings.java
index 43a758f..552d310 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowSettings.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowSettings.java
@@ -9,6 +9,7 @@
 import static android.os.Build.VERSION_CODES.Q;
 import static android.os.Build.VERSION_CODES.R;
 import static android.provider.Settings.Secure.LOCATION_MODE_OFF;
+import static org.robolectric.util.reflector.Reflector.reflector;
 
 import android.content.ContentResolver;
 import android.content.Context;
@@ -32,8 +33,8 @@
 import org.robolectric.annotation.Implementation;
 import org.robolectric.annotation.Implements;
 import org.robolectric.annotation.Resetter;
-import org.robolectric.shadow.api.Shadow;
-import org.robolectric.util.ReflectionHelpers.ClassParameter;
+import org.robolectric.util.reflector.ForType;
+import org.robolectric.util.reflector.Static;
 
 @SuppressWarnings({"UnusedDeclaration"})
 @Implements(Settings.class)
@@ -267,11 +268,7 @@
           && RuntimeEnvironment.getApiLevel() >= KITKAT
           && RuntimeEnvironment.getApiLevel() < P) {
         // Map from to underlying location provider storage API to location mode
-        return Shadow.directlyOn(
-            Settings.Secure.class,
-            "getLocationModeForUser",
-            ClassParameter.from(ContentResolver.class, cr),
-            ClassParameter.from(int.class, 0));
+        return reflector(SettingsSecureReflector.class).getLocationModeForUser(cr, 0);
       }
 
       return get(Integer.class, name).orElseThrow(() -> new SettingNotFoundException(name));
@@ -283,11 +280,7 @@
           && RuntimeEnvironment.getApiLevel() >= KITKAT
           && RuntimeEnvironment.getApiLevel() < P) {
         // Map from to underlying location provider storage API to location mode
-        return Shadow.directlyOn(
-            Settings.Secure.class,
-            "getLocationModeForUser",
-            ClassParameter.from(ContentResolver.class, cr),
-            ClassParameter.from(int.class, 0));
+        return reflector(SettingsSecureReflector.class).getLocationModeForUser(cr, 0);
       }
 
       return get(Integer.class, name).orElse(def);
@@ -576,4 +569,10 @@
   public static void reset() {
     canDrawOverlays = false;
   }
+
+  @ForType(Settings.Secure.class)
+  interface SettingsSecureReflector {
+    @Static
+    int getLocationModeForUser(ContentResolver cr, int userId);
+  }
 }
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowSubscriptionManager.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowSubscriptionManager.java
index ff81de5..529aaa4 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowSubscriptionManager.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowSubscriptionManager.java
@@ -6,6 +6,8 @@
 import static android.os.Build.VERSION_CODES.O_MR1;
 import static android.os.Build.VERSION_CODES.P;
 import static android.os.Build.VERSION_CODES.Q;
+import static android.os.Build.VERSION_CODES.R;
+import static android.os.Build.VERSION_CODES.TIRAMISU;
 
 import android.os.Build.VERSION;
 import android.telephony.SubscriptionInfo;
@@ -32,11 +34,20 @@
   public static final int INVALID_PHONE_INDEX =
       ReflectionHelpers.getStaticField(SubscriptionManager.class, "INVALID_PHONE_INDEX");
 
+  private static int activeDataSubscriptionId = SubscriptionManager.INVALID_SUBSCRIPTION_ID;
   private static int defaultSubscriptionId = SubscriptionManager.INVALID_SUBSCRIPTION_ID;
   private static int defaultDataSubscriptionId = SubscriptionManager.INVALID_SUBSCRIPTION_ID;
   private static int defaultSmsSubscriptionId = SubscriptionManager.INVALID_SUBSCRIPTION_ID;
   private static int defaultVoiceSubscriptionId = SubscriptionManager.INVALID_SUBSCRIPTION_ID;
 
+  private final Map<Integer, String> phoneNumberMap = new HashMap<>();
+
+  /** Returns value set with {@link #setActiveDataSubscriptionId(int)}. */
+  @Implementation(minSdk = R)
+  protected static int getActiveDataSubscriptionId() {
+    return activeDataSubscriptionId;
+  }
+
   /** Returns value set with {@link #setDefaultSubscriptionId(int)}. */
   @Implementation(minSdk = N)
   protected static int getDefaultSubscriptionId() {
@@ -85,6 +96,11 @@
     return defaultDataSubscriptionId;
   }
 
+  /** Sets the value that will be returned by {@link #getActiveDataSubscriptionId()}. */
+  public static void setActiveDataSubscriptionId(int activeDataSubscriptionId) {
+    ShadowSubscriptionManager.activeDataSubscriptionId = activeDataSubscriptionId;
+  }
+
   /** Sets the value that will be returned by {@link #getDefaultSubscriptionId()}. */
   public static void setDefaultSubscriptionId(int defaultSubscriptionId) {
     ShadowSubscriptionManager.defaultSubscriptionId = defaultSubscriptionId;
@@ -109,8 +125,8 @@
   private static Map<Integer, Integer> phoneIds = new HashMap<>();
 
   /**
-   * Cache of {@link SubscriptionInfo} used by {@link #getActiveSubscriptionInfoList}.
-   * Managed by {@link #setActiveSubscriptionInfoList}.
+   * Cache of {@link SubscriptionInfo} used by {@link #getActiveSubscriptionInfoList}. Managed by
+   * {@link #setActiveSubscriptionInfoList}.
    */
   private List<SubscriptionInfo> subscriptionList = new ArrayList<>();
   /**
@@ -212,6 +228,7 @@
   /**
    * Sets the active list of {@link SubscriptionInfo}. This call internally triggers {@link
    * OnSubscriptionsChangedListener#onSubscriptionsChanged()} to all the listeners.
+   *
    * @param list - The subscription info list, can be null.
    */
   public void setActiveSubscriptionInfoList(List<SubscriptionInfo> list) {
@@ -299,7 +316,7 @@
   }
 
   /** Clears the local cache of roaming subscription Ids used by {@link #isNetworkRoaming}. */
-  public void clearNetworkRoamingStatus(){
+  public void clearNetworkRoamingStatus() {
     roamingSimSubscriptionIds.clear();
   }
 
@@ -377,8 +394,25 @@
     }
   }
 
+  /**
+   * Returns the phone number for the given {@code subscriptionId}, or an empty string if not
+   * available.
+   *
+   * <p>The phone number can be set by {@link #setPhoneNumber(int, String)}
+   */
+  @Implementation(minSdk = TIRAMISU)
+  protected String getPhoneNumber(int subscriptionId) {
+    return phoneNumberMap.getOrDefault(subscriptionId, "");
+  }
+
+  /** Sets the phone number returned by {@link #getPhoneNumber(int)}. */
+  public void setPhoneNumber(int subscriptionId, String phoneNumber) {
+    phoneNumberMap.put(subscriptionId, phoneNumber);
+  }
+
   @Resetter
   public static void reset() {
+    activeDataSubscriptionId = SubscriptionManager.INVALID_SUBSCRIPTION_ID;
     defaultDataSubscriptionId = SubscriptionManager.INVALID_SUBSCRIPTION_ID;
     defaultSmsSubscriptionId = SubscriptionManager.INVALID_SUBSCRIPTION_ID;
     defaultVoiceSubscriptionId = SubscriptionManager.INVALID_SUBSCRIPTION_ID;
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowSystemVibrator.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowSystemVibrator.java
index 840c626..cce1990 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowSystemVibrator.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowSystemVibrator.java
@@ -162,30 +162,30 @@
 
   private void recordVibratePredefined(long milliseconds, int effectId) {
     vibrating = true;
-    this.effectId = effectId;
-    this.milliseconds = milliseconds;
+    ShadowVibrator.effectId = effectId;
+    ShadowVibrator.milliseconds = milliseconds;
     handler.removeCallbacks(stopVibratingRunnable);
-    handler.postDelayed(stopVibratingRunnable, this.milliseconds);
+    handler.postDelayed(stopVibratingRunnable, ShadowVibrator.milliseconds);
   }
 
   private void recordVibrate(long milliseconds) {
     vibrating = true;
-    this.milliseconds = milliseconds;
+    ShadowVibrator.milliseconds = milliseconds;
     handler.removeCallbacks(stopVibratingRunnable);
-    handler.postDelayed(stopVibratingRunnable, this.milliseconds);
+    handler.postDelayed(stopVibratingRunnable, ShadowVibrator.milliseconds);
   }
 
   protected void recordVibratePattern(long[] pattern, int repeat) {
     vibrating = true;
-    this.pattern = pattern;
-    this.repeat = repeat;
+    ShadowVibrator.pattern = pattern;
+    ShadowVibrator.repeat = repeat;
     handler.removeCallbacks(stopVibratingRunnable);
     if (repeat < 0) {
       long endDelayMillis = 0;
       for (long t : pattern) {
         endDelayMillis += t;
       }
-      this.milliseconds = endDelayMillis;
+      ShadowVibrator.milliseconds = endDelayMillis;
       handler.postDelayed(stopVibratingRunnable, endDelayMillis);
     }
   }
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowTelephonyManager.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowTelephonyManager.java
index e7499ea..dc456f3 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowTelephonyManager.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowTelephonyManager.java
@@ -131,6 +131,7 @@
   private int carrierIdFromSimMccMnc;
   private String subscriberId;
   private /*UiccSlotInfo[]*/ Object uiccSlotInfos;
+  private /*UiccCardInfo[]*/ Object uiccCardsInfo;
   private String visualVoicemailPackageName = null;
   private SignalStrength signalStrength;
   private boolean dataEnabled = false;
@@ -163,7 +164,8 @@
     callComposerStatus = 0;
   }
 
-  public static void setCallComposerStatus(int callComposerStatus) {
+  @Implementation(minSdk = S)
+  protected void setCallComposerStatus(int callComposerStatus) {
     ShadowTelephonyManager.callComposerStatus = callComposerStatus;
   }
 
@@ -487,6 +489,18 @@
     return uiccSlotInfos;
   }
 
+  /** Sets the UICC cards information returned by {@link #getUiccCardsInfo()}. */
+  public void setUiccCardsInfo(/*UiccCardsInfo[]*/ Object uiccCardsInfo) {
+    this.uiccCardsInfo = uiccCardsInfo;
+  }
+
+  /** Returns the UICC cards information set by {@link #setUiccCardsInfo}. */
+  @Implementation(minSdk = Q)
+  @HiddenApi
+  protected /*UiccSlotInfo[]*/ Object getUiccCardsInfo() {
+    return uiccCardsInfo;
+  }
+
   /** Clears {@code slotIndex} to state mapping and resets to default state. */
   public void resetSimStates() {
     simStates.clear();
@@ -898,6 +912,7 @@
    */
   @Implementation(minSdk = O)
   protected TelephonyManager createForPhoneAccountHandle(PhoneAccountHandle handle) {
+    checkReadPhoneStatePermission();
     return phoneAccountToTelephonyManagers.get(handle);
   }
 
@@ -1075,6 +1090,9 @@
    */
   @Implementation(minSdk = Build.VERSION_CODES.Q)
   protected boolean isEmergencyNumber(String number) {
+    if (ShadowServiceManager.getService(Context.TELEPHONY_SERVICE) == null) {
+      throw new IllegalStateException("telephony service is null.");
+    }
 
     if (number == null) {
       return false;
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowTypeface.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowTypeface.java
index 58abb58..5db6c9d 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowTypeface.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowTypeface.java
@@ -1,262 +1,21 @@
 package org.robolectric.shadows;
 
-import static android.os.Build.VERSION_CODES.KITKAT;
-import static android.os.Build.VERSION_CODES.LOLLIPOP;
-import static android.os.Build.VERSION_CODES.N_MR1;
-import static android.os.Build.VERSION_CODES.O;
-import static android.os.Build.VERSION_CODES.O_MR1;
-import static android.os.Build.VERSION_CODES.P;
-import static android.os.Build.VERSION_CODES.Q;
-import static android.os.Build.VERSION_CODES.R;
-import static android.os.Build.VERSION_CODES.S;
-import static org.robolectric.RuntimeEnvironment.getApiLevel;
-import static org.robolectric.Shadows.shadowOf;
-
-import android.annotation.SuppressLint;
-import android.content.res.AssetManager;
-import android.graphics.FontFamily;
 import android.graphics.Typeface;
-import android.util.ArrayMap;
-import java.io.File;
-import java.io.IOException;
-import java.nio.file.Files;
-import java.nio.file.Path;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.Map;
-import java.util.Objects;
-import java.util.concurrent.atomic.AtomicLong;
-import org.robolectric.RuntimeEnvironment;
-import org.robolectric.annotation.HiddenApi;
-import org.robolectric.annotation.Implementation;
 import org.robolectric.annotation.Implements;
-import org.robolectric.annotation.RealObject;
-import org.robolectric.annotation.Resetter;
-import org.robolectric.res.Fs;
-import org.robolectric.shadow.api.Shadow;
-import org.robolectric.util.ReflectionHelpers;
-import org.robolectric.util.ReflectionHelpers.ClassParameter;
+import org.robolectric.shadows.ShadowTypeface.Picker;
 
-@Implements(value = Typeface.class, looseSignatures = true)
-@SuppressLint("NewApi")
-public class ShadowTypeface {
-  private static final Map<Long, FontDesc> FONTS = Collections.synchronizedMap(new HashMap<>());
-  private static final AtomicLong nextFontId = new AtomicLong(1);
-  private FontDesc description;
-
-  @HiddenApi
-  @Implementation(maxSdk = KITKAT)
-  protected void __constructor__(int fontId) {
-    description = findById((long) fontId);
-  }
-
-  @HiddenApi
-  @Implementation(minSdk = LOLLIPOP)
-  protected void __constructor__(long fontId) {
-    description = findById(fontId);
-  }
-
-  @Implementation
-  protected static void __staticInitializer__() {
-    Shadow.directInitialize(Typeface.class);
-    if (RuntimeEnvironment.getApiLevel() > R) {
-      Typeface.loadPreinstalledSystemFontMap();
-    }
-  }
-
-  @Implementation(minSdk = P)
-  protected static Typeface create(Typeface family, int weight, boolean italic) {
-    if (family == null) {
-      return createUnderlyingTypeface(null, weight);
-    } else {
-      ShadowTypeface shadowTypeface = Shadow.extract(family);
-      return createUnderlyingTypeface(shadowTypeface.getFontDescription().getFamilyName(), weight);
-    }
-  }
-
-  @Implementation
-  protected static Typeface create(String familyName, int style) {
-    return createUnderlyingTypeface(familyName, style);
-  }
-
-  @Implementation
-  protected static Typeface create(Typeface family, int style) {
-    if (family == null) {
-      return createUnderlyingTypeface(null, style);
-    } else {
-      ShadowTypeface shadowTypeface = Shadow.extract(family);
-      return createUnderlyingTypeface(shadowTypeface.getFontDescription().getFamilyName(), style);
-    }
-  }
-
-  @Implementation
-  protected static Typeface createFromAsset(AssetManager mgr, String path) {
-    ShadowAssetManager shadowAssetManager = Shadow.extract(mgr);
-    Collection<Path> assetDirs = shadowAssetManager.getAllAssetDirs();
-    for (Path assetDir : assetDirs) {
-      Path assetFile = assetDir.resolve(path);
-      if (Files.exists(assetFile)) {
-        return createUnderlyingTypeface(path, Typeface.NORMAL);
-      }
-
-      // maybe path is e.g. "myFont", but we should match "myFont.ttf" too?
-      Path[] files;
-      try {
-        files = Fs.listFiles(assetDir, f -> f.getFileName().toString().startsWith(path));
-      } catch (IOException e) {
-        throw new RuntimeException(e);
-      }
-      if (files.length != 0) {
-        return createUnderlyingTypeface(path, Typeface.NORMAL);
-      }
-    }
-
-    throw new RuntimeException("Font asset not found " + path);
-  }
-
-  @Implementation(minSdk = O, maxSdk = P)
-  protected static Typeface createFromResources(AssetManager mgr, String path, int cookie) {
-    return createUnderlyingTypeface(path, Typeface.NORMAL);
-  }
-
-  @Implementation(minSdk = O)
-  protected static Typeface createFromResources(
-      Object /* FamilyResourceEntry */ entry,
-      Object /* AssetManager */ mgr,
-      Object /* String */ path) {
-    return createUnderlyingTypeface((String) path, Typeface.NORMAL);
-  }
-
-  @Implementation
-  protected static Typeface createFromFile(File path) {
-    String familyName = path.toPath().getFileName().toString();
-    return createUnderlyingTypeface(familyName, Typeface.NORMAL);
-  }
-
-  @Implementation
-  protected static Typeface createFromFile(String path) {
-    return createFromFile(new File(path));
-  }
-
-  @Implementation
-  protected int getStyle() {
-    return description.getStyle();
-  }
-
-  @Override
-  @Implementation
-  public boolean equals(Object o) {
-    if (o instanceof Typeface) {
-      Typeface other = ((Typeface) o);
-      return Objects.equals(getFontDescription(), shadowOf(other).getFontDescription());
-    }
-    return false;
-  }
-
-  @Override
-  @Implementation
-  public int hashCode() {
-    return getFontDescription().hashCode();
-  }
-
-  @HiddenApi
-  @Implementation(minSdk = LOLLIPOP)
-  protected static Typeface createFromFamilies(Object /*FontFamily[]*/ families) {
-    return null;
-  }
-
-  @HiddenApi
-  @Implementation(minSdk = LOLLIPOP, maxSdk = N_MR1)
-  protected static Typeface createFromFamiliesWithDefault(Object /*FontFamily[]*/ families) {
-    return null;
-  }
-
-  @Implementation(minSdk = O, maxSdk = O_MR1)
-  protected static Typeface createFromFamiliesWithDefault(
-      Object /*FontFamily[]*/ families, Object /* int */ weight, Object /* int */ italic) {
-    return createUnderlyingTypeface("fake-font", Typeface.NORMAL);
-  }
-
-  @Implementation(minSdk = P)
-  protected static Typeface createFromFamiliesWithDefault(
-      Object /*FontFamily[]*/ families,
-      Object /* String */ fallbackName,
-      Object /* int */ weight,
-      Object /* int */ italic) {
-    return createUnderlyingTypeface((String) fallbackName, Typeface.NORMAL);
-  }
-
-  @Implementation(minSdk = P, maxSdk = P)
-  protected static void buildSystemFallback(
-      String xmlPath,
-      String fontDir,
-      ArrayMap<String, Typeface> fontMap,
-      ArrayMap<String, FontFamily[]> fallbackMap) {
-    fontMap.put("sans-serif", createUnderlyingTypeface("sans-serif", 0));
-  }
-
-  /** Avoid spurious error message about /system/etc/fonts.xml */
-  @Implementation(minSdk = LOLLIPOP, maxSdk = O_MR1)
-  protected static void init() {}
-
-  @HiddenApi
-  @Implementation(minSdk = Q, maxSdk = R)
-  public static void initSystemDefaultTypefaces(
-      Object systemFontMap, Object fallbacks, Object aliases) {}
-
-  @Resetter
-  public static synchronized void reset() {
-    FONTS.clear();
-  }
-
-  protected static Typeface createUnderlyingTypeface(String familyName, int style) {
-    long thisFontId = nextFontId.getAndIncrement();
-    FONTS.put(thisFontId, new FontDesc(familyName, style));
-    if (getApiLevel() >= LOLLIPOP) {
-      return ReflectionHelpers.callConstructor(
-          Typeface.class, ClassParameter.from(long.class, thisFontId));
-    } else {
-      return ReflectionHelpers.callConstructor(
-          Typeface.class, ClassParameter.from(int.class, (int) thisFontId));
-    }
-  }
-
-  private static synchronized FontDesc findById(long fontId) {
-    if (FONTS.containsKey(fontId)) {
-      return FONTS.get(fontId);
-    }
-    throw new RuntimeException("Unknown font id: " + fontId);
-  }
-
-  @Implementation(minSdk = O, maxSdk = R)
-  protected static long nativeCreateFromArray(long[] familyArray, int weight, int italic) {
-    // TODO: implement this properly
-    long thisFontId = nextFontId.getAndIncrement();
-    FONTS.put(thisFontId, new FontDesc(null, weight));
-    return thisFontId;
-  }
+/** Base class for {@link ShadowTypeface} classes. */
+@Implements(value = Typeface.class, shadowPicker = Picker.class)
+public abstract class ShadowTypeface {
 
   /**
    * Returns the font description.
    *
    * @return Font description.
    */
-  public FontDesc getFontDescription() {
-    return description;
-  }
+  public abstract FontDesc getFontDescription();
 
-  @Implementation(minSdk = S)
-  protected static void nativeForceSetStaticFinalField(String fieldname, Typeface typeface) {
-    ReflectionHelpers.setStaticField(Typeface.class, fieldname, typeface);
-  }
-
-  @Implementation(minSdk = S)
-  protected static long nativeCreateFromArray(
-      long[] familyArray, long fallbackTypeface, int weight, int italic) {
-    return ShadowTypeface.nativeCreateFromArray(familyArray, weight, italic);
-  }
-
+  /** Contains data about a font. */
   public static class FontDesc {
     public final String familyName;
     public final int style;
@@ -305,15 +64,10 @@
     }
   }
 
-  /** Shadow for {@link Typeface.Builder} */
-  @Implements(value = Typeface.Builder.class, minSdk = Q)
-  public static class ShadowBuilder {
-    @RealObject Typeface.Builder realBuilder;
-
-    @Implementation
-    protected Typeface build() {
-      String path = ReflectionHelpers.getField(realBuilder, "mPath");
-      return createUnderlyingTypeface(path, Typeface.NORMAL);
+  /** Shadow picker for {@link Typeface}. */
+  public static final class Picker extends GraphicsShadowPicker<Object> {
+    public Picker() {
+      super(ShadowLegacyTypeface.class, ShadowNativeTypeface.class);
     }
   }
 }
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowUserManager.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowUserManager.java
index 5a17708..2c283d6 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowUserManager.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowUserManager.java
@@ -6,7 +6,6 @@
 import static android.os.Build.VERSION_CODES.M;
 import static android.os.Build.VERSION_CODES.N;
 import static android.os.Build.VERSION_CODES.N_MR1;
-import static android.os.Build.VERSION_CODES.O;
 import static android.os.Build.VERSION_CODES.P;
 import static android.os.Build.VERSION_CODES.Q;
 import static android.os.Build.VERSION_CODES.R;
@@ -855,7 +854,7 @@
    * <p>This method checks whether the user handle corresponds to a managed profile, and then query
    * its state. When quiet, the user is not running.
    */
-  @Implementation(minSdk = O)
+  @Implementation(minSdk = N)
   protected boolean isQuietModeEnabled(UserHandle userHandle) {
     // Return false if this is not a managed profile (this is the OS's behavior).
     if (!isManagedProfileWithoutPermission(userHandle)) {
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowVibrator.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowVibrator.java
index 3159bf9..b66a0a4 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowVibrator.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowVibrator.java
@@ -14,31 +14,32 @@
 import javax.annotation.Nullable;
 import org.robolectric.annotation.Implementation;
 import org.robolectric.annotation.Implements;
+import org.robolectric.annotation.Resetter;
 
 @Implements(Vibrator.class)
 public class ShadowVibrator {
-  boolean vibrating;
-  boolean cancelled;
-  long milliseconds;
-  protected long[] pattern;
-  protected final List<VibrationEffectSegment> vibrationEffectSegments = new ArrayList<>();
-  protected final List<PrimitiveEffect> primitiveEffects = new ArrayList<>();
-  protected final List<Integer> supportedPrimitives = new ArrayList<>();
-  @Nullable protected VibrationAttributes vibrationAttributesFromLastVibration;
-  @Nullable protected AudioAttributes audioAttributesFromLastVibration;
-  int repeat;
-  boolean hasVibrator = true;
-  boolean hasAmplitudeControl = false;
-  int effectId;
+  static boolean vibrating;
+  static boolean cancelled;
+  static long milliseconds;
+  protected static long[] pattern;
+  protected static final List<VibrationEffectSegment> vibrationEffectSegments = new ArrayList<>();
+  protected static final List<PrimitiveEffect> primitiveEffects = new ArrayList<>();
+  protected static final List<Integer> supportedPrimitives = new ArrayList<>();
+  @Nullable protected static VibrationAttributes vibrationAttributesFromLastVibration;
+  @Nullable protected static AudioAttributes audioAttributesFromLastVibration;
+  static int repeat;
+  static boolean hasVibrator = true;
+  static boolean hasAmplitudeControl = false;
+  static int effectId;
 
   /** Controls the return value of {@link Vibrator#hasVibrator()} the default is true. */
   public void setHasVibrator(boolean hasVibrator) {
-    this.hasVibrator = hasVibrator;
+    ShadowVibrator.hasVibrator = hasVibrator;
   }
 
   /** Controls the return value of {@link Vibrator#hasAmplitudeControl()} the default is false. */
   public void setHasAmplitudeControl(boolean hasAmplitudeControl) {
-    this.hasAmplitudeControl = hasAmplitudeControl;
+    ShadowVibrator.hasAmplitudeControl = hasAmplitudeControl;
   }
 
   /**
@@ -119,6 +120,23 @@
     return audioAttributesFromLastVibration;
   }
 
+  @Resetter
+  public static void reset() {
+    vibrating = false;
+    cancelled = false;
+    milliseconds = 0;
+    pattern = null;
+    vibrationEffectSegments.clear();
+    primitiveEffects.clear();
+    supportedPrimitives.clear();
+    vibrationAttributesFromLastVibration = null;
+    audioAttributesFromLastVibration = null;
+    repeat = 0;
+    hasVibrator = true;
+    hasAmplitudeControl = false;
+    effectId = 0;
+  }
+
   /**
    * A data class for exposing {@link VibrationEffect.Composition$PrimitiveEffect}, which is a
    * hidden non TestApi class introduced in Android R.
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowViewRootImpl.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowViewRootImpl.java
index 921d278..cfe2bc7 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowViewRootImpl.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowViewRootImpl.java
@@ -21,9 +21,11 @@
 import android.view.SurfaceControl;
 import android.view.View;
 import android.view.ViewRootImpl;
+import android.view.WindowInsets;
 import android.view.WindowManager;
 import android.window.ClientWindowFrames;
 import java.util.ArrayList;
+import java.util.Optional;
 import org.robolectric.RuntimeEnvironment;
 import org.robolectric.annotation.Implementation;
 import org.robolectric.annotation.Implements;
@@ -47,6 +49,49 @@
 
   @RealObject protected ViewRootImpl realObject;
 
+  /**
+   * The visibility of the system status bar.
+   *
+   * <p>The value will be read in the intercepted {@link #getWindowInsets(boolean)} method providing
+   * the current state via the returned {@link WindowInsets} instance if it has been set..
+   *
+   * <p>NOTE: This state does not reflect the current state of system UI visibility flags or the
+   * current window insets. Rather it tracks the latest known state provided via {@link
+   * #setIsStatusBarVisible(boolean)}.
+   */
+  private static Optional<Boolean> isStatusBarVisible = Optional.empty();
+
+  /**
+   * The visibility of the system navigation bar.
+   *
+   * <p>The value will be read in the intercepted {@link #getWindowInsets(boolean)} method providing
+   * the current state via the returned {@link WindowInsets} instance if it has been set.
+   *
+   * <p>NOTE: This state does not reflect the current state of system UI visibility flags or the
+   * current window insets. Rather it tracks the latest known state provided via {@link
+   * #setIsNavigationBarVisible(boolean)}.
+   */
+  private static Optional<Boolean> isNavigationBarVisible = Optional.empty();
+
+  /** Allows other shadows to set the state of {@link #isStatusBarVisible}. */
+  protected static void setIsStatusBarVisible(boolean isStatusBarVisible) {
+    ShadowViewRootImpl.isStatusBarVisible = Optional.of(isStatusBarVisible);
+  }
+
+  /** Clears the last known state of {@link #isStatusBarVisible}. */
+  protected static void clearIsStatusBarVisible() {
+    ShadowViewRootImpl.isStatusBarVisible = Optional.empty();
+  }
+
+  /** Allows other shadows to set the state of {@link #isNavigationBarVisible}. */
+  protected static void setIsNavigationBarVisible(boolean isNavigationBarVisible) {
+    ShadowViewRootImpl.isNavigationBarVisible = Optional.of(isNavigationBarVisible);
+  }
+
+  /** Clears the last known state of {@link #isNavigationBarVisible}. */
+  protected static void clearIsNavigationBarVisible() {
+    ShadowViewRootImpl.isNavigationBarVisible = Optional.empty();
+  }
 
   @Implementation(maxSdk = VERSION_CODES.JELLY_BEAN)
   protected static IWindowSession getWindowSession(Looper mainLooper) {
@@ -185,6 +230,38 @@
     }
   }
 
+  /**
+   * On Android R+ {@link WindowInsets} supports checking visibility of specific inset types.
+   *
+   * <p>For those SDK levels, override the real {@link WindowInsets} with the tracked system bar
+   * visibility status ({@link #isStatusBarVisible}/{@link #isNavigationBarVisible}), if set.
+   *
+   * <p>NOTE: We use state tracking in place of a longer term solution of implementing the insets
+   * calculations and broadcast (via listeners) for now. Once we have insets calculations working we
+   * should remove this mechanism.
+   */
+  @Implementation(minSdk = R)
+  protected WindowInsets getWindowInsets(boolean forceConstruct) {
+    WindowInsets realInsets =
+        reflector(ViewRootImplReflector.class, realObject).getWindowInsets(forceConstruct);
+
+    WindowInsets.Builder overridenInsetsBuilder = new WindowInsets.Builder(realInsets);
+
+    if (isStatusBarVisible.isPresent()) {
+      overridenInsetsBuilder =
+          overridenInsetsBuilder.setVisible(
+              WindowInsets.Type.statusBars(), isStatusBarVisible.get());
+    }
+
+    if (isNavigationBarVisible.isPresent()) {
+      overridenInsetsBuilder =
+          overridenInsetsBuilder.setVisible(
+              WindowInsets.Type.navigationBars(), isNavigationBarVisible.get());
+    }
+
+    return overridenInsetsBuilder.build();
+  }
+
   @Resetter
   public static void reset() {
     ViewRootImplReflector viewRootImplStatic = reflector(ViewRootImplReflector.class);
@@ -192,6 +269,9 @@
     viewRootImplStatic.setFirstDrawHandlers(new ArrayList<>());
     viewRootImplStatic.setFirstDrawComplete(false);
     viewRootImplStatic.setConfigCallbacks(new ArrayList<>());
+
+    clearIsStatusBarVisible();
+    clearIsNavigationBarVisible();
   }
 
   public void callWindowFocusChanged(boolean hasFocus) {
@@ -389,5 +469,9 @@
 
     // SDK >= T
     void windowFocusChanged(boolean hasFocus);
+
+    // SDK >= M
+    @Direct
+    WindowInsets getWindowInsets(boolean forceConstruct);
   }
 }
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowWallpaperManager.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowWallpaperManager.java
index 87cb0f2..648b449 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowWallpaperManager.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowWallpaperManager.java
@@ -1,5 +1,9 @@
 package org.robolectric.shadows;
 
+import static android.os.Build.VERSION_CODES.M;
+import static android.os.Build.VERSION_CODES.N;
+import static android.os.Build.VERSION_CODES.TIRAMISU;
+
 import android.Manifest.permission;
 import android.annotation.FloatRange;
 import android.annotation.RequiresPermission;
@@ -61,12 +65,12 @@
    * <p>This only caches the resource id in memory. Calling this will override any previously set
    * resource and does not differentiate between users.
    */
-  @Implementation(maxSdk = VERSION_CODES.M)
+  @Implementation(maxSdk = M)
   protected void setResource(int resid) {
     setResource(resid, WallpaperManager.FLAG_SYSTEM | WallpaperManager.FLAG_LOCK);
   }
 
-  @Implementation(minSdk = VERSION_CODES.N)
+  @Implementation(minSdk = N)
   protected int setResource(int resid, int which) {
     if ((which & (WallpaperManager.FLAG_SYSTEM | WallpaperManager.FLAG_LOCK)) == 0) {
       return 0;
@@ -100,18 +104,21 @@
    * @param which either {@link WallpaperManager#FLAG_LOCK} or {WallpaperManager#FLAG_SYSTEM}
    * @return 0 if fails to cache. Otherwise, 1.
    */
-  @Implementation(minSdk = VERSION_CODES.P)
+  @Implementation(minSdk = N)
   protected int setBitmap(Bitmap fullImage, Rect visibleCropHint, boolean allowBackup, int which) {
-    if (which == WallpaperManager.FLAG_LOCK) {
+    if ((which & (WallpaperManager.FLAG_SYSTEM | WallpaperManager.FLAG_LOCK)) == 0) {
+      return 0;
+    }
+    if ((which & WallpaperManager.FLAG_LOCK) == WallpaperManager.FLAG_LOCK) {
       lockScreenImage = fullImage;
       wallpaperInfo = null;
-      return 1;
-    } else if (which == WallpaperManager.FLAG_SYSTEM) {
+    }
+
+    if ((which & WallpaperManager.FLAG_SYSTEM) == WallpaperManager.FLAG_SYSTEM) {
       homeScreenImage = fullImage;
       wallpaperInfo = null;
-      return 1;
     }
-    return 0;
+    return 1;
   }
 
   /**
@@ -138,7 +145,7 @@
    * @return An open, readable file descriptor to the requested wallpaper image file; {@code null}
    *     if no such wallpaper is configured.
    */
-  @Implementation(minSdk = VERSION_CODES.P)
+  @Implementation(minSdk = N)
   @Nullable
   protected ParcelFileDescriptor getWallpaperFile(int which) {
     if (which == WallpaperManager.FLAG_SYSTEM && homeScreenImage != null) {
@@ -149,7 +156,7 @@
     return null;
   }
 
-  @Implementation(minSdk = VERSION_CODES.N)
+  @Implementation(minSdk = N)
   protected boolean isSetWallpaperAllowed() {
     return isWallpaperAllowed;
   }
@@ -158,7 +165,7 @@
     isWallpaperAllowed = allowed;
   }
 
-  @Implementation(minSdk = VERSION_CODES.M)
+  @Implementation(minSdk = M)
   protected boolean isWallpaperSupported() {
     return isWallpaperSupported;
   }
@@ -176,17 +183,20 @@
    * @param which either {@link WallpaperManager#FLAG_LOCK} or {WallpaperManager#FLAG_SYSTEM}
    * @return 0 if fails to cache. Otherwise, 1.
    */
-  @Implementation(minSdk = VERSION_CODES.N)
+  @Implementation(minSdk = N)
   protected int setStream(
       InputStream bitmapData, Rect visibleCropHint, boolean allowBackup, int which) {
-    if (which == WallpaperManager.FLAG_LOCK) {
-      lockScreenImage = BitmapFactory.decodeStream(bitmapData);
-      return 1;
-    } else if (which == WallpaperManager.FLAG_SYSTEM) {
-      homeScreenImage = BitmapFactory.decodeStream(bitmapData);
-      return 1;
+    if ((which & (WallpaperManager.FLAG_SYSTEM | WallpaperManager.FLAG_LOCK)) == 0) {
+      return 0;
     }
-    return 0;
+    if ((which & WallpaperManager.FLAG_LOCK) == WallpaperManager.FLAG_LOCK) {
+      lockScreenImage = BitmapFactory.decodeStream(bitmapData);
+    }
+
+    if ((which & WallpaperManager.FLAG_SYSTEM) == WallpaperManager.FLAG_SYSTEM) {
+      homeScreenImage = BitmapFactory.decodeStream(bitmapData);
+    }
+    return 1;
   }
 
   /**
@@ -196,7 +206,7 @@
    * previously set static wallpaper.
    */
   @SystemApi
-  @Implementation(minSdk = VERSION_CODES.M)
+  @Implementation(minSdk = M)
   @RequiresPermission(permission.SET_WALLPAPER_COMPONENT)
   protected boolean setWallpaperComponent(ComponentName wallpaperService)
       throws IOException, XmlPullParserException {
@@ -222,17 +232,17 @@
    * Returns the information about the wallpaper if the current wallpaper is a live wallpaper
    * component. Otherwise, if the wallpaper is a static image, this returns null.
    */
-  @Implementation(minSdk = VERSION_CODES.M)
+  @Implementation
   protected WallpaperInfo getWallpaperInfo() {
     return wallpaperInfo;
   }
 
-  @Implementation(minSdk = VERSION_CODES.TIRAMISU)
+  @Implementation(minSdk = TIRAMISU)
   protected void setWallpaperDimAmount(@FloatRange(from = 0f, to = 1f) float dimAmount) {
     wallpaperDimAmount = MathUtils.saturate(dimAmount);
   }
 
-  @Implementation(minSdk = VERSION_CODES.TIRAMISU)
+  @Implementation(minSdk = TIRAMISU)
   @FloatRange(from = 0f, to = 1f)
   protected float getWallpaperDimAmount() {
     return wallpaperDimAmount;
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowWifiManager.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowWifiManager.java
index 83ca1fb..561ed90 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowWifiManager.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowWifiManager.java
@@ -65,6 +65,7 @@
   private final ConcurrentHashMap<WifiManager.OnWifiUsabilityStatsListener, Executor>
       wifiUsabilityStatsListeners = new ConcurrentHashMap<>();
   private final List<WifiUsabilityScore> usabilityScores = new ArrayList<>();
+  private Object networkScorer;
   @RealObject WifiManager wifiManager;
   private WifiConfiguration apConfig;
 
@@ -436,6 +437,32 @@
     }
   }
 
+  /**
+   * Implements setWifiConnectedNetworkScorer() with the generic Object input as
+   * WifiConnectedNetworkScorer is a hidden/System API.
+   */
+  @Implementation(minSdk = R)
+  @HiddenApi
+  protected boolean setWifiConnectedNetworkScorer(Object executorObject, Object scorerObject) {
+    if (networkScorer == null) {
+      networkScorer = scorerObject;
+      return true;
+    } else {
+      return false;
+    }
+  }
+
+  @Implementation(minSdk = R)
+  @HiddenApi
+  protected void clearWifiConnectedNetworkScorer() {
+    networkScorer = null;
+  }
+
+  /** Returns if wifi connected betwork scorer enabled */
+  public boolean isWifiConnectedNetworkScorerEnabled() {
+    return networkScorer != null;
+  }
+
   @Implementation
   protected boolean setWifiApConfiguration(WifiConfiguration apConfig) {
     this.apConfig = apConfig;
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowWindowManagerGlobal.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowWindowManagerGlobal.java
index 80b484c..75b0371 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowWindowManagerGlobal.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowWindowManagerGlobal.java
@@ -2,40 +2,25 @@
 
 import static android.os.Build.VERSION_CODES.JELLY_BEAN_MR1;
 import static android.os.Build.VERSION_CODES.JELLY_BEAN_MR2;
-import static android.os.Build.VERSION_CODES.LOLLIPOP_MR1;
-import static android.os.Build.VERSION_CODES.M;
 import static android.os.Build.VERSION_CODES.P;
-import static android.os.Build.VERSION_CODES.Q;
 import static android.os.Build.VERSION_CODES.R;
-import static android.os.Build.VERSION_CODES.S;
-import static android.os.Build.VERSION_CODES.S_V2;
 import static org.robolectric.shadows.ShadowView.useRealGraphics;
 import static org.robolectric.util.reflector.Reflector.reflector;
 
 import android.app.Instrumentation;
 import android.content.ClipData;
 import android.content.Context;
-import android.graphics.Rect;
 import android.os.Binder;
-import android.os.Build.VERSION;
 import android.os.IBinder;
 import android.os.Looper;
 import android.os.RemoteException;
 import android.os.ServiceManager;
-import android.view.DisplayCutout;
-import android.view.IWindow;
 import android.view.IWindowManager;
 import android.view.IWindowSession;
-import android.view.InputChannel;
-import android.view.InsetsSourceControl;
-import android.view.InsetsState;
-import android.view.InsetsVisibilities;
-import android.view.Surface;
-import android.view.SurfaceControl;
 import android.view.View;
-import android.view.WindowManager;
 import android.view.WindowManagerGlobal;
 import androidx.annotation.Nullable;
+import java.lang.reflect.Proxy;
 import org.robolectric.RuntimeEnvironment;
 import org.robolectric.annotation.Implementation;
 import org.robolectric.annotation.Implements;
@@ -53,44 +38,18 @@
     minSdk = JELLY_BEAN_MR1,
     looseSignatures = true)
 public class ShadowWindowManagerGlobal {
-  private static WindowSessionDelegate windowSessionDelegate;
+  private static WindowSessionDelegate windowSessionDelegate = new WindowSessionDelegate();
   private static IWindowSession windowSession;
 
   @Resetter
   public static void reset() {
     reflector(WindowManagerGlobalReflector.class).setDefaultWindowManager(null);
-    windowSessionDelegate = null;
+    windowSessionDelegate = new WindowSessionDelegate();
     windowSession = null;
   }
 
-  private static synchronized WindowSessionDelegate getWindowSessionDelegate() {
-    if (windowSessionDelegate == null) {
-      int apiLevel = RuntimeEnvironment.getApiLevel();
-      if (apiLevel >= S_V2) {
-        windowSessionDelegate = new WindowSessionDelegateSV2();
-      } else if (apiLevel >= S) {
-        windowSessionDelegate = new WindowSessionDelegateS();
-      } else if (apiLevel >= R) {
-        windowSessionDelegate = new WindowSessionDelegateR();
-      } else if (apiLevel >= Q) {
-        windowSessionDelegate = new WindowSessionDelegateQ();
-      } else if (apiLevel >= P) {
-        windowSessionDelegate = new WindowSessionDelegateP();
-      } else if (apiLevel >= M) {
-        windowSessionDelegate = new WindowSessionDelegateM();
-      } else if (apiLevel >= LOLLIPOP_MR1) {
-        windowSessionDelegate = new WindowSessionDelegateLMR1();
-      } else if (apiLevel >= JELLY_BEAN_MR1) {
-        windowSessionDelegate = new WindowSessionDelegateJBMR1();
-      } else {
-        windowSessionDelegate = new WindowSessionDelegateJB();
-      }
-    }
-    return windowSessionDelegate;
-  }
-
   public static boolean getInTouchMode() {
-    return getWindowSessionDelegate().getInTouchMode();
+    return windowSessionDelegate.getInTouchMode();
   }
 
   /**
@@ -98,7 +57,7 @@
    * Instrumentation#setInTouchMode(boolean)} to modify this from a test.
    */
   static void setInTouchMode(boolean inTouchMode) {
-    getWindowSessionDelegate().setInTouchMode(inTouchMode);
+    windowSessionDelegate.setInTouchMode(inTouchMode);
   }
 
   /**
@@ -107,21 +66,46 @@
    */
   @Nullable
   public static ClipData getLastDragClipData() {
-    return windowSessionDelegate != null ? windowSessionDelegate.lastDragClipData : null;
+    return windowSessionDelegate.lastDragClipData;
   }
 
   /** Clears the data returned by {@link #getLastDragClipData()}. */
   public static void clearLastDragClipData() {
-    if (windowSessionDelegate != null) {
-      windowSessionDelegate.lastDragClipData = null;
-    }
+    windowSessionDelegate.lastDragClipData = null;
   }
 
   @Implementation(minSdk = JELLY_BEAN_MR2)
   protected static synchronized IWindowSession getWindowSession() {
     if (windowSession == null) {
+      // Use Proxy.newProxyInstance instead of ReflectionHelpers.createDelegatingProxy as there are
+      // too many variants of 'add', 'addToDisplay', and 'addToDisplayAsUser', some of which have
+      // arg types that don't exist any more.
       windowSession =
-          ReflectionHelpers.createDelegatingProxy(IWindowSession.class, getWindowSessionDelegate());
+          (IWindowSession)
+              Proxy.newProxyInstance(
+                  IWindowSession.class.getClassLoader(),
+                  new Class<?>[] {IWindowSession.class},
+                  (proxy, method, args) -> {
+                    String methodName = method.getName();
+                    switch (methodName) {
+                      case "add": // SDK 16
+                      case "addToDisplay": // SDK 17-29
+                      case "addToDisplayAsUser": // SDK 30+
+                        return windowSessionDelegate.getAddFlags();
+                      case "getInTouchMode":
+                        return windowSessionDelegate.getInTouchMode();
+                      case "performDrag":
+                        return windowSessionDelegate.performDrag(args);
+                      case "prepareDrag":
+                        return windowSessionDelegate.prepareDrag();
+                      case "setInTouchMode":
+                        windowSessionDelegate.setInTouchMode((boolean) args[0]);
+                        return null;
+                      default:
+                        return ReflectionHelpers.defaultValueForType(
+                            method.getReturnType().getName());
+                    }
+                  });
     }
     return windowSession;
   }
@@ -143,7 +127,7 @@
     if (service == null) {
       service = IWindowManager.Stub.asInterface(ServiceManager.getService(Context.WINDOW_SERVICE));
       reflector(WindowManagerGlobalReflector.class).setWindowManagerService(service);
-      if (VERSION.SDK_INT >= 30) {
+      if (RuntimeEnvironment.getApiLevel() >= R) {
         reflector(WindowManagerGlobalReflector.class).setUseBlastAdapter(service.useBLAST());
       }
     }
@@ -169,7 +153,7 @@
     void setUseBlastAdapter(boolean useBlastAdapter);
   }
 
-  private abstract static class WindowSessionDelegate {
+  private static class WindowSessionDelegate {
     // From WindowManagerGlobal (was WindowManagerImpl in JB).
     static final int ADD_FLAG_IN_TOUCH_MODE = 0x1;
     static final int ADD_FLAG_APP_VISIBLE = 0x2;
@@ -202,200 +186,20 @@
       this.inTouchMode = inTouchMode;
     }
 
-    // @Implementation(maxSdk = O_MR1)
-    public IBinder prepareDrag(
-        IWindow window, int flags, int thumbnailWidth, int thumbnailHeight, Surface outSurface) {
+    public IBinder prepareDrag() {
       return new Binder();
     }
 
-    // @Implementation(maxSdk = M)
-    public boolean performDrag(
-        IWindow window,
-        IBinder dragToken,
-        float touchX,
-        float touchY,
-        float thumbCenterX,
-        float thumbCenterY,
-        ClipData data) {
-      lastDragClipData = data;
-      return true;
-    }
-
-    // @Implementation(minSdk = N, maxSdk = O_MR1)
-    public boolean performDrag(
-        IWindow window,
-        IBinder dragToken,
-        int touchSource,
-        float touchX,
-        float touchY,
-        float thumbCenterX,
-        float thumbCenterY,
-        ClipData data) {
-      lastDragClipData = data;
-      return true;
-    }
-  }
-
-  private static class WindowSessionDelegateJB extends WindowSessionDelegate {
-    // @Implementation(maxSdk = JELLY_BEAN)
-    public int add(
-        IWindow window,
-        int seq,
-        WindowManager.LayoutParams attrs,
-        int viewVisibility,
-        int layerStackId,
-        Rect outContentInsets,
-        InputChannel outInputChannel) {
-      return getAddFlags();
-    }
-  }
-
-  private static class WindowSessionDelegateJBMR1 extends WindowSessionDelegateJB {
-    // @Implementation(minSdk = JELLY_BEAN_MR1, maxSdk = LOLLIPOP)
-    public int addToDisplay(
-        IWindow window,
-        int seq,
-        WindowManager.LayoutParams attrs,
-        int viewVisibility,
-        int layerStackId,
-        Rect outContentInsets,
-        InputChannel outInputChannel) {
-      return getAddFlags();
-    }
-  }
-
-  private static class WindowSessionDelegateLMR1 extends WindowSessionDelegateJBMR1 {
-    // @Implementation(sdk = LOLLIPOP_MR1)
-    public int addToDisplay(
-        IWindow window,
-        int seq,
-        WindowManager.LayoutParams attrs,
-        int viewVisibility,
-        int layerStackId,
-        Rect outContentInsets,
-        Rect outStableInsets,
-        InputChannel outInputChannel) {
-      return getAddFlags();
-    }
-  }
-
-  private static class WindowSessionDelegateM extends WindowSessionDelegateLMR1 {
-    // @Implementation(minSdk = M, maxSdk = O_MR1)
-    public int addToDisplay(
-        IWindow window,
-        int seq,
-        WindowManager.LayoutParams attrs,
-        int viewVisibility,
-        int layerStackId,
-        Rect outContentInsets,
-        Rect outStableInsets,
-        Rect outInsets,
-        InputChannel outInputChannel) {
-      return getAddFlags();
-    }
-  }
-
-  private static class WindowSessionDelegateP extends WindowSessionDelegateM {
-    // @Implementation(sdk = P)
-    public int addToDisplay(
-        IWindow window,
-        int seq,
-        WindowManager.LayoutParams attrs,
-        int viewVisibility,
-        int layerStackId,
-        Rect outFrame,
-        Rect outContentInsets,
-        Rect outStableInsets,
-        Rect outOutsets,
-        DisplayCutout.ParcelableWrapper displayCutout,
-        InputChannel outInputChannel) {
-      return getAddFlags();
-    }
-
-    // @Implementation(minSdk = P)
-    public IBinder performDrag(
-        IWindow window,
-        int flags,
-        SurfaceControl surface,
-        int touchSource,
-        float touchX,
-        float touchY,
-        float thumbCenterX,
-        float thumbCenterY,
-        ClipData data) {
-      lastDragClipData = data;
-      return new Binder();
-    }
-  }
-
-  private static class WindowSessionDelegateQ extends WindowSessionDelegateP {
-    // @Implementation(sdk = Q)
-    public int addToDisplay(
-        IWindow window,
-        int seq,
-        WindowManager.LayoutParams attrs,
-        int viewVisibility,
-        int layerStackId,
-        Rect outFrame,
-        Rect outContentInsets,
-        Rect outStableInsets,
-        Rect outOutsets,
-        DisplayCutout.ParcelableWrapper displayCutout,
-        InputChannel outInputChannel,
-        InsetsState insetsState) {
-      return getAddFlags();
-    }
-  }
-
-  private static class WindowSessionDelegateR extends WindowSessionDelegateQ {
-    // @Implementation(sdk = R)
-    public int addToDisplayAsUser(
-        IWindow window,
-        int seq,
-        WindowManager.LayoutParams attrs,
-        int viewVisibility,
-        int layerStackId,
-        int userId,
-        Rect outFrame,
-        Rect outContentInsets,
-        Rect outStableInsets,
-        DisplayCutout.ParcelableWrapper displayCutout,
-        InputChannel outInputChannel,
-        InsetsState insetsState,
-        InsetsSourceControl[] activeControls) {
-      return getAddFlags();
-    }
-  }
-
-  private static class WindowSessionDelegateS extends WindowSessionDelegateR {
-    // @Implementation(sdk = S)
-    public int addToDisplayAsUser(
-        IWindow window,
-        WindowManager.LayoutParams attrs,
-        int viewVisibility,
-        int layerStackId,
-        int userId,
-        InsetsState requestedVisibility,
-        InputChannel outInputChannel,
-        InsetsState insetsState,
-        InsetsSourceControl[] activeControls) {
-      return getAddFlags();
-    }
-  }
-
-  private static class WindowSessionDelegateSV2 extends WindowSessionDelegateS {
-    // @Implementation(minSdk = S_V2)
-    public int addToDisplayAsUser(
-        IWindow window,
-        WindowManager.LayoutParams attrs,
-        int viewVisibility,
-        int displayId,
-        int userId,
-        InsetsVisibilities requestedVisibilities,
-        InputChannel outInputChannel,
-        InsetsState outInsetsState,
-        InsetsSourceControl[] outActiveControls) {
-      return getAddFlags();
+    public Object performDrag(Object[] args) {
+      // extract the clipData param
+      for (int i = args.length - 1; i >= 0; i--) {
+        if (args[i] instanceof ClipData) {
+          lastDragClipData = (ClipData) args[i];
+          // In P (SDK 28), the return type changed from boolean to Binder.
+          return RuntimeEnvironment.getApiLevel() >= P ? new Binder() : true;
+        }
+      }
+      throw new AssertionError("Missing ClipData param");
     }
   }
 }